简体   繁体   中英

An example of functional programming in scala

I'm studying scala. It's very promising, thanks to Odersky and all the other authors for their great work.

I took a euler problem ( http://projecteuler.net/ ) to have a more-then-minimal example. And I'm trying to go the functional way. So this is not a "please answer me immediatly or my boss will kill me" but a "please if you've got time, can you help a imperative language programmer to take a journey in the functional world?"

Problem: I want a class for poker hands. A poker Hand is composed by a number of Card, from 0 to 5. I'd like to build the list of Cards one and for all, that is: my Hand class will be immutable, if I want to add a card, then I create a new Hand object. So I need a collection of Card that can be created as "val", not as var. First step: constructors, one for each number of cards. But Card's collection is handled in each constructor, so I must have it as var!

Here's the code, Card class is simply a Suit and a Value, passed to constructor as a string ("5S" is the 5 of spades):

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 */
}

Do you know how to make it the true functional way? Thanks.

PS: if you really want to know, it's problem 54.

My answer is not about functional aspect of scala, but your code is possible to write shortly using 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* in auxiliary constructor says that you can have variable number of arguments (even thousand of strings). I'm getting input and invoke creation for each new Card with map function, and then pass result to parent constructor which has it's own requirement . (BTW, mapping from string to Card can be done anonymously, in that manner: this(input.map(new Card(_)).toList) )

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

Is equable to

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

From now on, if you will try to create more than five cards in hand you'll get 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)

Well, the the var in the code below comes from you not initializing cards from the main constructor:

// 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)
}

So what you need to do is fix the secondary constructor:

// 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)
}

THe problemn is simple: you want a list of cards formed by non-null Strings. If I were you, I'd just avoid passing nulls, but... Anyway, the best way to handle that is to convert this into options. The conversion is simple: Option(a) will return Some(a) is a is not null, and None if it is. If you compose a list of that, you can then flatten it to remove the None and convert Some(a) back into a . In other words:

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

Because in this example you are only allowed to use five cards I would check this at compile time with the use of a 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) }

Of course in another example when there can be an unspecific number of elements you need to check them at runtime. productIterator only returns a Iterator[Any] but when you use your cards directly by the field-identifiers (_1.. _5) you will get an ACard .

Firstly, null is evil, use Option instead. Secondly, Scala supports default parameters. So instead of creating all the constructors, you might just wanna use one of them like this:

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

or with Option , which is safer.

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)
}

So the cards are only added to the buffer if they "exist".

First, we need to fix a compile error in your cards field definition.

Note that in Scala you usually don't have to declare fields. Main constructor parameters already are fields, So: this can be written simpler:

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

Now we have mutability problem. If you want to program in functional style, everything should be immutable, so the "full hand constructor" is not functional at all: it has 6 side-effecting operations, last of which does not compile.

In functional setting, an object can't be modified after its constructor has terminated, so all the code after this(Nil) is useless. You already said that cards is Nil , what else do you want?, So, all the computations have to happen before the main constructor call. We'd like to remove this(Nil) from the top and add this(sortCards(cardBuffer.toList)) to the bottom. Unfortunately, Scala does not allow that. Fortunately, it allows more options to achieve the same than java: first, you can use a nested block like this:

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

second, you can use apply method instead of a constructor:

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

Now, let's start getting rid of imperative ListBuffer . First improvement would be to use var of type List[Card] . Making mutability more local will help to remove it later:

// 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)

Okay, now we can see what exactly we are mutating and can easily remove that mutability:

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)

Now we have a fair amount of code duplication. Let's remove that in a brute-force way (find a long duplicating piece of code and extract function)!

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)

Next, very important, transformation would be to replace nullable references with the values of Option type, or remove the empty card concept altogether.

Update:

As requested, I am adding an example of usage of the apply method. Notice that it is declared in object Hand , not class Hand , so it doesn't need an instance of the class (it's similar to static method in java). We just apply the object to parameters: val hand = Hand("5S", "5S", "5S", "5S", "5S") .

Attempt using varargs, overloaded + operator, repeated addition in constructor, and Set to eliminate duplicate cards.

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)
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM