代码之家  ›  专栏  ›  技术社区  ›  The Archetypal Paul

功能Scala的重构/布局

  •  12
  • The Archetypal Paul  · 技术社区  · 14 年前

    这一行。。。

     Console.println(io.Source.fromFile("names.txt").getLines.mkString.split(",").map{x:String => x.slice(1, x.length -1)}.sortBy { x => x}.zipWithIndex.map{t =>{ (t._2 +1)*(t._1.map{_.toChar - "A"(0).toChar + 1}.sum)}}.sum);
    

    ... 是我的解决方案 Project Euler problem 22 . 它看起来很管用,而且是用(我的尝试)功能风格写的。

    这个例子有点极端,但我的问题有点笼统——您更喜欢编写/格式化/注释函数式代码吗?函数式方法似乎鼓励了一系列方法调用,我发现这些调用可能无法读取,也不会留下明显的注释。

    另外,当我编写过程代码时,我发现我编写的每个小方法都有一个目的和有意义的名称。当我写函数代码的时候,我似乎养成了一种习惯,这种习惯产生的代码行有点像上面的代码行,其中(对我来说)含义很难理解,而且个别计算也很难在其他地方重用。我在网上看到的很多函数代码示例都很简洁(对我来说)晦涩难懂。

    我该怎么办?用当前上下文中有意义的名称为计算的每个部分编写一些函数?(即使它们只是地图的包装,比如说?)

    就我举的例子来说,有什么更好的方式来写它,并展示它?

    (和所有的风格问题一样,这个问题是主观的。不过,它没有理由要争论

    4 回复  |  直到 14 年前
        1
  •  19
  •   Kevin Wright    14 年前

    整理它的第一个小小的尝试就是去掉前导 Console. 尾随者 ; 以及明确的 :String 类型-所有这些都是不必要的-添加一些缩进并导入io。源:

    import io.Source
    println(
      Source.fromFile("names.txt").getLines.mkString.split(",").map{
        x => x.slice(1, x.length -1)
      }.sortBy {x => x}.zipWithIndex.map{
        t =>{ (t._2 +1)*(t._1.map{_.toChar - "A"(0).toChar + 1}.sum)}
      }.sum
    )
    

    下一步是稍微清理一下,在映射元组列表和 identity 而不是 x=>x . toChar 对于字符也是不必要的,单引号可用于表示字符文本。

    import io.Source
    println(
      Source.fromFile("names.txt").getLines.mkString.split(",").map {
        x => x.slice(1, x.length -1)
      }.sortBy(identity).zipWithIndex.map {
        case (v, idx) =>{ (idx+1)*(v.map{_ - 'A' + 1}.sum)}
      }.sum
    )
    

    再做一些修改也有助于使代码的意图更加清晰:

    import io.Source
    println(
      Source.fromFile("names.txt").getLines.mkString.split(",")
      .map { _.stripPrefix("\"").stripSuffix("\"") }
      .sortBy(identity)
      .map { _.map{_ - 'A' + 1}.sum }
      .zipWithIndex
      .map { case (v, idx) => (idx+1) * v }
      .sum
    )
    

    下一步,使它更“实用”,就是把它分解成“实用”( 鬼鬼祟祟的,嗯? ). 理想情况下,每个函数都有一个明确表达其用途的名称,并且名称很短(目标是使其成为单个表达式,因此不需要大括号):

    import io.Source
    
    def unquote(s:String) = s.stripPrefix("\"").stripSuffix("\"")
    
    def wordsFrom(fname:String) =
      Source.fromFile(fname).getLines.mkString.split(",").map(unquote)
    
    def letterPos(c:Char) = c - 'A' + 1
    
    println(
      wordsFrom("names.txt")
      .sortBy(identity)
      .map { _.map(letterPos).sum }
      .zipWithIndex
      .map { case (v, idx) => (idx+1) * v }
      .sum
    )
    

    wordsFrom 是一个明显的1行程序,但是为了在stackOverflow上更容易格式化,我将它拆分了

        2
  •  4
  •   Brian    14 年前

    以下是我认为最好的布局方式:

    Console.println(
        io.Source.fromFile("names.txt")
        .getLines.mkString.split(",")
        .map{x:String => x.slice(1, x.length -1)}
        .sortBy { x => x}
        .zipWithIndex
        .map{t =>{ (t._2 +1)*(t._1.map{_.toChar - "A"(0).toChar + 1}.sum)}}
        .sum); 
    

    我觉得在我的大脑深处,有一些算法可以在水平空间和垂直空间之间做出代码布局权衡的决定,但我似乎没有直接的途径来表达它。:)

    关于引入名称而不是使用lambda,我认为我通常做的是,如果我想用一个简短的注释来描述代码的意图,但是一个好的标识符名称也可以这样做,那么我可以将一个一次性lambda放入一个命名函数中,以获得标识符名称的可读性好处。与 toChar map ,但是 地图 或者,引入垂直空格可以让您在 //comment 这是引入标识符名称的替代方法。

    (免责声明:我不写Scala,所以如果我说的任何东西与风格约定相冲突,请忽略我:),但我认为很多建议基本上是语言不可知的。)

        3
  •  4
  •   Daniel C. Sobral    14 年前

    严格地讲如何 格式 代码,不做任何结构更改,我会这样做:

    Console println (
      (
        io.Source
        fromFile "names.txt"
        getLines ()
        mkString ""
        split ","
        map (x => x.slice(1, x.length - 1))
        sortBy (x => x)
        zipWithIndex
      )
      map (t => (t._2 + 1) * (t._1 map (_.toChar - "A"(0).toChar + 1) sum) )
      sum
    )
    

    或者,也许,为了避开无参数方法,我会这样做:

    Console println (
      io.Source
      .fromFile("names.txt")
      .getLines
      .mkString
      .split(",")
      .map(x => x.slice(1, x.length - 1))
      .sortBy(x => x)
      .zipWithIndex
      .map(t => (t._2 + 1) * (t._1 map (_.toChar - "A"(0).toChar + 1) sum) )
      .sum
    )
    

    请注意,有足够的空间发表评论,但是,一般来说 正在做的事情通常是清楚的。不习惯的人有时会迷路 中途,没有变量来跟踪 转换后的值。

    现在,我会做一些不同的事情:

    println ( // instead of Console println
      Source // put import elsewhere
      .fromFile("names.txt")
      .mkString // Unless you want to get rid of /n, which is unnecessary here
      .split(",")
      .map(x => x.slice(1, x.length - 1))
      .sorted // instead of sortBy
      .zipWithIndex
      .map { // use { only for multiple statements and, as in this case, pattern matching
        case (c, index) => (index + 1) * (c map (_ - 'A' + 1) sum) // chars are chars
      }
      .sum
    )
    

    我也不会在同一步中求和和和乘法,所以:

      .sorted
      .map(_ map (_ - 'A' + 1) sum)
      .zipWithIndex
      .map { case (av, index) => av * (index + 1) }
      .sum
    

    最后,我不太喜欢调整字符串大小,所以我可能会使用regex。再加上一点重构,我可能会这样写:

      import scala.io.Source
      def names = Source fromFile "names.txt" mkString
    
      def wordExtractor = """"(.*?)"""".r
      def words = for {
        m <- wordExtractor findAllIn names matchData
      } yield m group 1
    
      def alphabeticValue(s: String) = s map (_ - 'A' + 1) sum
      def wordsAV = words.toList.sorted map alphabeticValue
    
      def multByIndex(t: (Int, Int)) = t match {
        case (av, index) => av * (index + 1)
      }
      def wordsAVByIndex = wordsAV.zipWithIndex map multByIndex
    
      println(wordsAVByIndex.sum)
    

    编辑

    下一步将是重命名重构——选择能更好地传达代码每个部分所做工作的名称。Ken在评论中建议了更好的名字,我会把它们用在另一个变体上(它也很好地展示了 更好的 命名提高了可读性)。

    import scala.io.Source
    def rawData = Source fromFile "names.txt" mkString
    
    // I'd rather write "match" than "m" in the next snippet, but
    // the former is a keyword in Scala, so "m" has become more
    // common in my code than "i". Also, make the return type of
    // getWordsOf clear, because iterators can be tricky.
    // Returning a list, however, makes a much less cleaner
    // definition.
    
    def wordExtractor = """"(.*?)"""".r
    def getWordsOf(input: String): Iterator[String] = for {
      m <- wordExtractor findAllIn input matchData
    } yield m group 1
    def wordList = getWordsOf(rawData).toList
    
    // I stole letterPosition from Kevin's solution. There, I said it. :-)
    
    def letterPosition(c: Char) = c.toUpper - 'A' + 1 // .toUpper isn't necessary
    def alphabeticValueOfWord(word: String) = word map letterPosition sum
    def alphabeticValues = wordList.sorted map alphabeticValueOfWord
    
    // I don't like multAVByIndex, but I haven't decided on a better
    // option yet. I'm not very fond of declaring methods that return
    // functions either, but I find that better than explicitly handling
    // tuples (oh, for the day tuples/arguments are unified!).
    
    def multAVByIndex = (alphabeticValue: Int, index: Int) => 
      alphabeticValue * (index + 1)
    def scores = alphabeticValues.zipWithIndex map multAVByIndex.tupled
    
    println(scores sum)
    
        4
  •  1
  •   Shiva Wu    14 年前

    除了凯文的解决方案,

    关键是在考虑到可重用性和可读性的情况下,清晰、整洁地划分功能。

    为了使代码更加简洁,似乎可以使用for表达式。

    
    def inputString(inputFile: String) = io.Source.fromFile(inputFile).getLines.mkString
    
    def inputWords(input: String) = input.split("[,\"]").filter("" != )
    
    Console.println {
        (for { (s, idx) <- inputWords(inputString("names.txt")).sorted.zipWithIndex }
            yield s.map(_ - 'A' + 1).sum * (idx + 1)).sum
    }
    

    s.map(-'A'+1)部分可以进一步放入一个函数中,比如wordSum,如果您希望它更加清晰。