【问题标题】:An example of functional programming in scalascala中的函数式编程示例
【发布时间】:2011-10-23 21:10:13
【问题描述】:

我正在学习 scala。非常有希望,感谢 Odersky 和所有其他作者的出色工作。

我拿了一个欧拉问题 (http://projecteuler.net/) 来举一个更简单的例子。而且我正在尝试采用功能性方式。所以这不是“请立即回答我,否则我的老板会杀了我”,而是“如果你有时间,你能帮助命令式语言程序员踏上函数式世界的旅程吗?”

问题:我想要一门扑克牌课程。扑克手由许多卡片组成,从 0 到 5。我想一劳永逸地构建卡片列表,即:我的 Hand 类将是不可变的,如果我想添加一张卡片,那么我创建了一个新的 Hand 对象。 所以我需要一个可以创建为“val”而不是 var 的 Card 集合。 第一步:构造函数,每张卡片一个。但是 Card 的集合是在每个构造函数中处理的,所以我必须将它作为 var!

代码如下,Card 类只是一个 Suit 和一个 Value,作为字符串传递给构造函数(“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。

【问题讨论】:

  • 通常,null 在功能上是不需要的。对于这种情况,Scala 使用 Option monad(Haskell 有 Maybe)。 Option tutorial
  • 在主构造函数中添加了 sordCard(),感谢@Rotsor 的帮助。

标签: scala functional-programming


【解决方案1】:

我的回答不是关于 scala 的 功能 方面,但您的代码可以使用 scala 糖很快编写:

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 函数为每个新卡获取输入并调用创建,然后将结果传递给具有自己要求的父构造函数。 (顺便说一句,从字符串到 Card 的 映射 可以匿名完成,以这种方式: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)a 不为空,如果是 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

      【讨论】:

      • 使用元组很有趣,即使@om-nom-nom 解决方案更简洁。但是您的解决方案仅提供带有 5 个参数的完整构造函数,这可能是一个问题。该示例需要计算手牌的等级,我设法做到了:搜索像一对这样的等级,如果有这样的等级,则从手牌中提取一对,然后迭代剩余的牌。要创建子手,我应该使用可变数量的 NoCard 调用完整的构造函数。也许这并不难,但变量 args 构造函数似乎是完美的选择。
      • 如果真的想表示在编译时检查的有限长度列表,更好的选择是Option[(Card, Option[(Card, Option[(Card, Option[(Card, Option[Card])])])])] :)
      • 虽然上面没有一些类型级别的魔法看起来很丑,但仍然可以使用简单的类型别名将其缩短为OC[OC[OC[OC[OC[Unit]]]]]
      【解决方案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)之后的所有代码都是无用的。你已经说过cardsNil,你还想要什么?!因此,所有的计算都必须发生在 主构造函数调用之前。我们想从顶部删除 this(Nil) 并将 this(sortCards(cardBuffer.toList)) 添加到底部。不幸的是,Scala 不允许这样做。幸运的是,它提供了比 java 更多的选项来实现相同的功能:首先,您可以使用这样的嵌套块:

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

      其次,您可以使用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。第一个改进是使用List[Card] 类型的var。使可变性更加本地化将有助于以后将其删除:

      // 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 类型的值,或者完全删除空卡概念。

      更新:

      根据要求,我添加了apply 方法的使用示例。请注意,它是在object Hand 中声明的,而不是class Hand,因此它不需要类的实例(类似于java 中的静态方法)。我们只是将对象应用于参数:val hand = Hand("5S", "5S", "5S", "5S", "5S")

      【讨论】:

      • 我的代码编译了,但是我为 sn-p 取出了太多行。你的回答是一个很好的关于消除可怕的紧迫性的教程,谢谢。我试过了,但我没有设法使用 apply 方法。我的理解是我需要一个手对象来使用它,但是不可能使用默认的 ctor,因为我最终得到了 2 个分配给卡片列表的对象。请提供一个简单的用法示例吗?
      • 添加apply的使用示例。我希望这会有所帮助。
      【解决方案6】:

      尝试使用可变参数、重载的 + 运算符、构造函数中的重复添加以及 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)
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-08-23
        • 1970-01-01
        • 2016-05-12
        • 2011-05-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-11-06
        相关资源
        最近更新 更多