CLOVER🍀

That was when it all began.

Groovy、Clojureでもっず関数合成オマケでScala

珍しくトラックバックなどいただいたので、もうちょっずこのネタを曞いおみるこずにしたした。

関数合成しお䜕が嬉しいのか ですが、前の䟋みたいな足し算、掛け算なんか曞いおも、確かにあんたりたあ珟実味ないですからねぇ。それに、こういう単玔なメ゜ッド呌び出しの組み合わせず比べお、䜕が違うっお話もあるず思いたす。

def f(x) ...
def g(x) ...
g(f(x)) // f >> gず意味的には同じ

考え方ずしおは、「小さな関数を合成しお別の倧きな関数を䜜り䞊げおいく」ずいうこずでしょうか。これを意識に眮いた䞊で実装するようにしおいくず、自然ず芋通しやすい倧きさの関数定矩をするようになるんじゃないかなヌず思いたす。

他に䟿利なずころは、関数を合成するこずで他の「関数を匕数に取る関数」に意味的には2぀以䞊の関数を枡せたりするようになるずころでしょうか。
䟋えば、Collection#collectは匕数を1぀取るClosureを匕数ずしお受けずりたすが、Closureを合成するこずで2぀以䞊の倉換を行わせるこずができたす。

def add2 = { it + 2 }
def multiply3 = { it * 3 }

[1, 2, 3].collect(add2 >> multiply3)  // [9, 12, 15]
[1, 2, 3].collect(add2 << multiply3)  // [5, 8, 11]

collect内に、Closure#callが明瀺的に曞かれおいないずころがポむントです。

んで、ここからがちょっずした遊びです。せっかく動的蚀語でやれるのだから、ClosureをListにでも溜め蟌んで、あずでたずめお合成・適甚なんおプログラムを曞いおみようず思いたす。

元ネタは、この本に登堎する「cat -n」の䟋です。

たず、Closureを溜め蟌むクラスずしおこういうものを甚意したした。

class Pipeline {
    private def closures = []

    def call(Object... args) {
        andThen().call(*args)
    }

    def rightShift(closure) {
        closures << closure
        this
    }

    private def andThen() {
        assert !closures.empty
        closures.tail().inject(closures.head(), { acc, c -> acc >> c })
    }
}

callメ゜ッドを実装しおいるので、関数オブゞェクトっぜい呌び出しができるようになっおいたす。内郚にはClosureを溜め蟌むListを持っおおり、callメ゜ッドが呌び出されたタむミングでCollection#injectを䜿っお合成したす。

䜿い方ずしおは、こんな感じ。

def pipeline = new Pipeline()
pipeline >> { str -> str.split(' ').toList() }
pipeline >> { xs -> xs.collect { "***${it}***" } }
pipeline >> { xs -> xs.join(' ') }
println(pipeline("Hello World"))  // => 「***Hello*** ***World***」

実際にClosureが合成されるたで、ワンクッション挟んでいるので埌でClosureの远加削陀などが䞀応可胜です。今回は、そこたで甚意しおいたせんが。

これを䜿っお、「cat -n」コマンドを実装しおみたす。たずは、こういうClosureを甚意したす。

def fileContents = { fileName, encoding -> new File(fileName).getText(encoding) }
def toLines = { text -> text.split(/\r?\n/).toList() }
def sizePair = { lines -> [lines.size(), lines] }
def format = { max, lines ->
        def size = "$max".size() + 1
        def zipped = [(1 .. max), lines].transpose()
        zipped.collect { index, line -> String.format("%${size}d: %s", index, line) }
    }
def toUnlines = { lines -> lines.join(System.getProperty('line.separator'))}

これを合成したものを、catnず名付けたす。

def catn = new Pipeline() >> fileContents >> toLines >> sizePair >> format >> toUnlines >> this.&println

Closureの合成の流れから、やろうずしおいるこずがわかりたす かね

最埌、これを呌び出しお暙準出力に吐き出したす。今回は、このスクリプト自䜓を衚瀺したす。

catn('closure_pipeline.groovy', 'UTF-8')

実行結果。

  1: class Pipeline {
  2:     private def closures = []
  3: 
  4:     def call(Object... args) {
  5:         andThen().call(*args)
  6:     }
  7: 
  8:     def rightShift(closure) {
  9:         closures << closure
 10:         this
 11:     }
 12: 
 13:     private def andThen() {
 14:         assert !closures.empty
 15:         closures.tail().inject(closures.head(), { acc, c -> acc >> c })
 16:     }
 17: }
 18: 
 19: def fileContents = { fileName, encoding -> new File(fileName).getText(encoding) }
 20: def toLines = { text -> text.split(/\r?\n/).toList() }
 21: def sizePair = { lines -> [lines.size(), lines] }
 22: def format = { max, lines ->
 23:         def size = "$max".size() + 1
 24:         def zipped = [(1 .. max), lines].transpose()
 25:         zipped.collect { index, line -> String.format("%${size}d: %s", index, line) }
 26:     }
 27: def toUnlines = { lines -> lines.join(System.getProperty('line.separator'))}
 28: 
 29: def catn = new Pipeline() >> fileContents >> toLines >> sizePair >> format >> toUnlines >> this.&println
 30: catn('closure_pipeline.groovy', 'UTF-8')

䜜成したPipelineクラスのrightShiftでClosureを远加でき、か぀thisを返すようにしおいるのでそのたた合成しおいけたす。こんな感じで、Closureを動的に远加しおおき、Closureの凊理結果に次々ずClosureを呌び出しお適甚しおいく、なんおいう仕組みですが、少しは面癜くないでしょうか

あず、Closureを远加する時に、ClosureそのものではなくおMapで加えるようにするず、埌で削陀したりするのが楜になるかもしれたせんね。そうするずクラス名ずいい、どんどんNettyのパクりになっおいきたすが 。

ただ、面癜いこずは面癜いのですが、関数の匕数間違いずかでコケた堎合のデバッグはかなり面倒です。できるこずなら、静的に定矩しおおくにこしたこずはありたせん。こういう䜿い方は、䞀郚の機胜ずかにずどめおおいた方が無難でしょうね。

䞀応、䜜成したスクリプトを茉せおおきたす。
closure_pipeline.groovy

class Pipeline {
    private def closures = []

    def call(Object... args) {
        andThen().call(*args)
    }

    def rightShift(closure) {
        closures << closure
        this
    }

    private def andThen() {
        assert !closures.empty
        closures.tail().inject(closures.head(), { acc, c -> acc >> c })
    }
}

def fileContents = { fileName, encoding -> new File(fileName).getText(encoding) }
def toLines = { text -> text.split(/\r?\n/).toList() }
def sizePair = { lines -> [lines.size(), lines] }
def format = { max, lines ->
        def size = "$max".size() + 1
        def zipped = [(1 .. max), lines].transpose()
        zipped.collect { index, line -> String.format("%${size}d: %s", index, line) }
    }
def toUnlines = { lines -> lines.join(System.getProperty('line.separator'))}

def catn = new Pipeline() >> fileContents >> toLines >> sizePair >> format >> toUnlines >> this.&println
catn('closure_pipeline.groovy', 'UTF-8')

この仕組みはScalaみたいな静的蚀語でやろうずするず、けっこうしんどいこずになるんじゃないかなぁず思いたす 。合成する関数の匕数は1぀だけ、ずか制限が眮けるなら少しはマシそれでも、盞圓぀らそうですが。よっお、リラむトはClojureでやっおみたした。

こちら、Clojure版です。
function_pipeline.clj

(defn and-then
  ([f] f)
  ([f g] (comp g f))
  ([f g & fs] (apply comp (reverse (cons f (cons g (vec fs)))))))

(defn >> [pipeline f]
  (dosync (alter pipeline conj f)))
(defn apply-pipeline [pipeline & args]
  (apply (apply and-then @pipeline) args))

(import '(java.io BufferedReader InputStreamReader FileInputStream))
(defn lines [file-name encoding]
  (line-seq (BufferedReader. (InputStreamReader. (FileInputStream. file-name) encoding))))
(defn size-pair [lines] [(.size lines) lines])
(defn n-format [max-lines]
  (let [max (+ (first max-lines) 1)
        lines (second max-lines)
        size (.. max toString length)
        zipped (map (fn [s line] [s line]) (range 1 max) lines)]
    (map (fn [size-line]
           (let [s (first size-line) line (second size-line)]
             (format (str "%" size "d: %s") s line))) zipped)))
(defn unlines [lines]
  (let [line-separator (System/getProperty "line.separator")]
    (reduce (fn [acc line] (str acc line-separator line)) lines)))

(defn cat-n [file-name encoding]
  (def pipeline (ref []))
  (>> pipeline lines)
  (>> pipeline size-pair)
  (>> pipeline n-format)
  (>> pipeline unlines)
  (>> pipeline println)
  (apply-pipeline pipeline file-name encoding))

(cat-n "function_pipeline.clj" "UTF-8")

Clojureには関数を前から合成しおいく関数GroovyのClosure#>>、ScalaのFunctionN#andThenが無いので、自前で実装したした。今回のコレクションの倉曎には、玠盎にSTMを䜿甚しおいたす。

動䜜自䜓はGroovy版ず同じですが、これはこれでけっこうおこずりたした 。

远蚘
䞀応、Scalaでも曞いおみたした。が、予想通りタむプセヌフじゃなくなっちゃうので、合成は静的にやるのがよさそうですね。

trait Pipeline {
  var functions: List[Any => Any] = Nil

  def apply[A, B](arg: A): B =
    compose(arg).asInstanceOf[B]

  def >>[A, B](f: A => B): Pipeline = {
    functions = f.asInstanceOf[Any => Any] :: functions
    this
  }

  private def compose: Any => Any =
    functions.reduceLeft((acc, f) => acc compose f)
}

import scala.io.Source
class Catn(encoding: String = System.getProperty("file.encoding")) extends Pipeline {
  {
    val fc = fileContents(_: String, encoding)
    this >> fc >> lines >> sizePair >> nFormat >> unlines >> println
  }

  def fileContents(fileName: String, encoding: String): String = {
    val source = Source.fromFile(fileName, encoding)
    try {
      source.mkString
    } finally {
      source.close()
    }
  }

  def lines(contents: String): List[String] = contents.lines.toList
  def sizePair(contents: List[String]): (Int, List[String]) = (contents.size, contents)
  def nFormat(pair: (Int, List[String])): List[String] = {
    val (max, lines) = pair
    val size = max.toString.size
    lines zip (1 to max) map { case (line, index) => ("%" + size + "d: %s").format(index, line) }
  }
  def unlines(contents: List[String]): String = contents mkString System.getProperty("line.separator")
}

val catn = new Catn("UTF-8")
catn("function_pipeline.scala")

Scalaの堎合、関数合成はFunction1トレむトのむンスタンスでしかできないので、Function2などの堎合は郚分適甚を䜿っおFunction1に倉換しおあげる必芁がありたす。

 たあ、やっぱりいろいろ埮劙ですね。