Пример функционального программирования в scala

Я изучаю scala. Это очень многообещающе, благодаря Одерскому и всем остальным авторам за их большую работу.

Я взял проблему эйлера (http://projecteuler.net/), чтобы иметь более-минимальный пример. И я пытаюсь идти по функциональному пути. Так что это не "пожалуйста, ответьте мне немедленно или мой босс убьет меня", но "пожалуйста, если у вас есть время, можете ли вы помочь императивному языковому программисту совершить путешествие в функциональном мире?"

Проблема: я хочу класс для рук в покере. Покерная рука состоит из нескольких карт, от 0 до 5. Я хотел бы создать список карт один и для всех, то есть: мой класс Руки будет неизменным, если я хочу добавить карту, то Я создаю новый объект Hand. Поэтому мне нужна коллекция Card, которую можно создать как "val", а не var. Первый шаг: конструкторы, по одному для каждого числа карт. Но сборка карт обрабатывается в каждом конструкторе, поэтому я должен иметь его как var!

Здесь код, класс карты - это просто Костюм и значение, переданные конструктору в виде строки ( "5S" - это 5 пиков):

class Hand(mycards : List[Card]) {
  // this should be val, I guess
  private var cards : List[Card] = {
    if (mycards.length>5)
      throw new IllegalArgumentException(
        "Illegal number of cards: " + mycards.length);
    sortCards(mycards)
  }

  // full hand constructor
  def this(a : String, b : String, c : String, d : String, e : String) = {
      this(Nil)

      // assign cards
      val cardBuffer = new ListBuffer[Card]()
      if ( a!=null ) cardBuffer += new Card(a)
      if ( b!=null ) cardBuffer += new Card(b)
      if ( c!=null ) cardBuffer += new Card(c)
      if ( d!=null ) cardBuffer += new Card(d)
      if ( e!=null ) cardBuffer += new Card(e)
      cards = sortCards(cardBuffer.toList)
  }
  // hand with less then 5 cards
  def this(a : String, b : String, c : String, d : String) = this(a,b,c,d,null)
  def this(a : String, b : String, c : String) = this(a, b, c, null)
  def this(a : String, b : String) = this(a, b, null)
  def this(a : String) = this(a, null)
  def this() = this(Nil)

/* removed */
}

Вы знаете, как сделать его истинным функциональным способом? Спасибо.

PS: если вы действительно хотите знать, это проблема 54.

Ответы

Ответ 1

Мой ответ не о функциональном аспекте scala, но ваш код можно быстро написать с помощью scala sugar:

class Hand(val mycards: List[Card]) {
  require (mycards.size <= 5,"can't be more than five cards")
  def this(input: String*) = { 
    this(input.map(c => new Card(c)).toList)
  }
}

input: String* в вспомогательном конструкторе говорит, что вы можете иметь переменное количество аргументов (даже тысячи строк). Я получаю ввод и вызываю создание для каждой новой Карты с помощью функции map, а затем передаю результат родительскому конструктору, у которого есть собственное требование. (BTW, отображение из строки в Карту можно сделать анонимно, таким образом: this(input.map(new Card(_)).toList))

class Hand(val mycards: List[Card]) {...

Подходит для

class Hand(cards: List[Card]) {
val mycards = cards 
...

С этого момента, если вы попытаетесь создать более пяти карт в руках, вы получите java.lang.IllegalArgumentException:

scala> class Card(s: String) {println("Im a :"+s)}
defined class Card

scala> new Hand("one","two","three","four","five","six")
Im a :one
Im a :two
Im a :three
Im a :four
Im a :five
Im a :six
java.lang.IllegalArgumentException: requirement failed: can't be more than five card
    at scala.Predef$.require(Predef.scala:157)
    at Hand.<init>(<console>:9)

Ответ 2

Ну, var в коде ниже проистекает из инициализации cards из основного конструктора:

// this should be val, I guess
private var cards : List[Card] = {
  if (mycards.length>5)
    throw new IllegalArgumentException(
      "Illegal number of cards: " + mycards.length);
  sortCards(mycards)
}

Итак, вам нужно исправить вторичный конструктор:

// full hand constructor
def this(a : String, b : String, c : String, d : String, e : String) = {
    this(Nil)

    // assign cards
    val cardBuffer = new ListBuffer[Card]()
    if ( a!=null ) cardBuffer += new Card(a)
    if ( b!=null ) cardBuffer += new Card(b)
    if ( c!=null ) cardBuffer += new Card(c)
    if ( d!=null ) cardBuffer += new Card(d)
    if ( e!=null ) cardBuffer += new Card(e)
    cards = sortCards(cardBuffer.toList)
}

Проблема проста: вам нужен список карт, образованных ненулевыми строками. Если бы я был вами, я бы просто избегал передачи нулей, но... В любом случае, лучший способ справиться с этим - это преобразовать это в параметры. Преобразование простое: Option(a) вернется Some(a) is a не равно null, а None, если оно есть. Если вы составите список этого, вы можете flatten удалить его None и преобразовать Some(a) обратно в a. Другими словами:

def this(a : String, b : String, c : String, d : String, e : String) = 
    this(List(a, b, c, d, e).map(Option(_)).flatten.map(Card(_)))

Ответ 3

Потому что в этом примере вам разрешено использовать только пять карт, которые я проверил бы во время компиляции с помощью Tuple5:

type HandType = (ACard, ACard, ACard, ACard, ACard)
case class Hand(h: HandType)

abstract class ACard {
  def exists: Boolean
}
case class Card(value: Int, color: Color) extends ACard {
  def exists = true
}
case object NoCard extends ACard {
  def exists = false
}

abstract class Color(val c: Int)
case object H extends Color(1)
case object C extends Color(2)
case object S extends Color(3)
case object D extends Color(4)
case object NoColor extends Color(0)

implicit def tuple2Card(t: (Int, Color)) = Card(t._1, t._2)

val h1 = Hand((Card(4, H), Card(6, S), Card(2, S), Card(8, D), NoCard))
val h2 = Hand((4 -> H, 6 -> S, 2 -> S, 8 -> D, NoCard))

println(h1)
println(h2)
h1.h.productIterator foreach { c => println(c.asInstanceOf[ACard].exists) }

Конечно, в другом примере, когда может быть неспецифическое количество элементов, вам нужно проверить их во время выполнения. productIterator возвращает только Iterator [Any], но когда вы используете свои карты непосредственно с помощью идентификаторов полей (_1.. _5), вы получите ACard.

Ответ 4

Во-первых, null является злом, вместо этого используйте Option. Во-вторых, Scala поддерживает параметры по умолчанию. Поэтому вместо создания всех конструкторов вы можете просто использовать один из них следующим образом:

def this(a: String = null, ..., e: String = null) = ...

или Option, что более безопасно.

def this(a: Option[String] = None, ..., e: Option[String] = None) = {
   this(Nil)

   val cardBuffer = new ListBuffer[Card]()
   a foreach { cardBuffer += new Card(_) }
   b foreach { cardBuffer += new Card(_) }
   c foreach { cardBuffer += new Card(_) }
   d foreach { cardBuffer += new Card(_) }
   e foreach { cardBuffer += new Card(_) }
   cards = sortCards(cardBuffer.toList)
}

Таким образом, карты добавляются только в буфер, если они "существуют".

Ответ 5

Во-первых, нам нужно исправить ошибку компиляции в определении поля cards.

Обратите внимание, что в Scala вам обычно не нужно объявлять поля. Основными параметрами конструктора уже являются поля! Таким образом, это можно записать проще:

class Hand(cards : List[Card]) {
  if (cards.length>5)
      throw new IllegalArgumentException(
        "Illegal number of cards: " + mycards.length);

Теперь у нас есть проблема мутируемости. Если вы хотите программировать в функциональном стиле, все должно быть неизменным, поэтому "конструктор полной руки" вообще не работает: он имеет 6 побочных операций, последний из которых не компилируется.

В функциональной настройке объект не может быть изменен после завершения его конструктора, поэтому весь код после this(Nil) бесполезен. Вы уже сказали, что cards есть Nil, что еще вы хотите?! Итак, все вычисления должны произойти до вызова главного конструктора. Мы хотели бы удалить this(Nil) сверху и добавить this(sortCards(cardBuffer.toList)) в конец. К сожалению, Scala не позволяет этого. К счастью, это позволяет больше возможностей достичь того же, чем java: во-первых, вы можете использовать вложенный блок следующим образом:

this({
  val cardBuffer = ... /* terrible imperativeness */
  sortCards(cardBuffer.toList)
})

second, вы можете использовать метод apply вместо конструктора:

object Hand {
  def apply(a : String, b : String, c : String, d : String, e : String) = {
    val cardBuffer = ... /* terrible imperativeness */
    new Hand(sortCards(cardBuffer.toList))
  }
}

Теперь давайте начнем избавляться от императива ListBuffer. Первым улучшением будет использование var типа List[Card]. Создание изменчивости более локально поможет удалить его позже:

// assign cards
var cards = Nil
if ( e!=null ) cards = new Card(e) :: cards
if ( d!=null ) cards = new Card(d) :: cards
if ( c!=null ) cards = new Card(c) :: cards
if ( b!=null ) cards = new Card(b) :: cards
if ( a!=null ) cards = new Card(a) :: cards
sortCards(cards)

Хорошо, теперь мы можем видеть, что именно мы мутируем и можем легко удалить эту изменчивость:

val fromE = if ( e!=null ) new Card(e) :: Nil else Nil
val fromD = if ( d!=null ) new Card(d) :: fromE else fromE
val fromC = if ( c!=null ) new Card(c) :: fromD else fromD
val fromB = if ( b!=null ) new Card(b) :: fromC else fromC
val fromA = if ( a!=null ) new Card(a) :: fromB else fromB
sortCards(fromA)

Теперь у нас есть достаточное количество дубликатов кода. Удалите это с помощью грубой силы (найдите длинный дублирующий фрагмент кода и функцию извлечения)!

def prependCard(x : String, cards : List[Card]) = 
  if ( x!=null ) new Card(x) :: cards else cards
val cards = prependCard(a, prependCard(b, 
              prependCard(c, prependCard(d, 
                prependCard(e, Nil)
              ))
            ))
sortCards(cards)

Далее, очень важно, преобразование было бы заменой нулевых ссылок значениями типа Option или вообще удалить концепцию пустой карты.

Update:

В соответствии с запросом я добавляю пример использования метода apply. Обратите внимание, что он объявлен в object Hand, а не class Hand, поэтому ему не нужен экземпляр класса (он похож на статический метод в java). Мы просто применяем объект к параметрам: val hand = Hand("5S", "5S", "5S", "5S", "5S").

Ответ 6

Попытка использования varargs, overloaded + operator, повторное добавление в конструкторе и Set для устранения дубликатов карт.

package poker

class Hand(private val cards:Set[Card] = Set.empty[Card]) {
  def + (card:Card) = {
    val hand = new Hand(cards + card)
    require(hand.length > length, "Card %s duplicated".format(card))
    require(hand.length <= Hand.maxLength, "Hand length > %d".format(Hand.maxLength))
    hand
  }
  def length = cards.size
  override def toString = cards.mkString("(", ",", ")")
}

object Hand {
  val maxLength = 5
  def apply(cards:Card*):Hand = cards.foldLeft(Hand())(_ + _)
  private def apply() = new Hand()
}

//-----------------------------------------------------------------------------------------------//


class Card private (override val toString:String) 

object Card {
  def apply(card:String) = {
    require(cardMap.contains(card), "Card %s does not exist".format(card))
    cardMap(card)
  }

  def cards = cardMap.values.toList

  private val cardMap = {
    val ranks = Range(2,9).inclusive.map { _.toString } ++ List("T", "J", "Q", "K", "A")
    val suits = List("c","d","h","s")
    (for(r <- ranks; s <- suits) yield (r + s -> new Card(r + s))).toMap
  } 
}

//-----------------------------------------------------------------------------------------------//

object Test extends App {
  Array("1f", "Ac").foreach { s =>
    try {
      printf("Created card %s successfully\n",Card(s))
    } catch {
      case e:Exception => printf("Input string %s - %s \n", s, e.getMessage)
    }
  }
  println

  for(i <- 0 to 6) {
    val cards = Card.cards.slice(0, i)
    makeHand(cards)
  }
  println

  val cards1 = List("Ac","Ad","Ac").map { Card(_) } 
  makeHand(cards1)
  println

  val hand1 = Hand(List("Ac","Ad").map { Card(_) }:_* )
  val card = Card("Ah")
  val hand2 = hand1 + card
  printf("%s + %s = %s\n", hand1, card, hand2)

  def makeHand(cards:List[Card]) =  
    try {
      val hand = Hand(cards: _*)
      printf("Created hand %s successfully\n",hand)
    } catch {
      case e:Exception => printf("Input %s - %s \n", cards, e.getMessage)
    }
}