better-files を使用して複数ファイルの一括処理を書き直す

会社の Scala エキスパートから、better-fs, fs2 というライブラリを教えていただいたので書き直してみる。とても短くなりました。(^q^)。

GitHub - pathikrit/better-files: Simple, safe and intuitive Scala I/O

ayakumo.hatenablog.com

@ import better.files._; import java.io.{File => JFile}
import better.files._;
import java.io.{File => JFile}

@ File("/tmp").glob("*.txt").foreach { file => file.lines.filter(_.contains("a")).foreach(println) }
cat
apple
banana

何にでも言えることだと思うけど初学者は知識も視野も狭いので、いわゆる「デファクトスタンダード」的な便利な非標準ライブラリまでたどり着かないことが多い。こういった面でメンターとなる方が側にいると大変ありがたい。(´ω`)

読み解き PiS3: Higher Order Functions を使って複数ファイルの一括処理を行う

9.4 WRITING NEW CONTROL STRUCTURES を読んでいて、複数ファイルに対して「ファイルを開いて特定の処理を行う」ことができるはずと思って実装した。Scala でメソッドをメソッドの引数に取るのは HOF(Higher Order Functions) というらしい。 C でいう関数ポインタがさらに高度になったようなイメージを持っている。

「ファイル名を受け取って、開く」までは共通だけれども、開いたファイルに対して実行したいことが異なる場合は 3 番目のメソッドだけ入れ替えればいいので柔軟性に富む。左から右に処理が書かれているのもわかりやすいかもしれない。⊂( ・∀・) 彡

$ cat /tmp/fruits.txt
apple
banana

$ cat /tmp/animals.txt
cat
dog
@ import scala.io.Source
import scala.io.Source

@ val files = List("/tmp/fruits.txt", "/tmp/animals.txt")
files: List[String] = List("/tmp/fruits.txt", "/tmp/animals.txt")

@ val getFileContents = (path: String) => Source.fromFile(path).getLines
getFileContents: String => Iterator[String] = ammonite.$sess.cmd2$$$Lambda$2031/1972193961@11a67420

@ def crunchFiles(paths: List[String], f1: String => Iterator[String], f2: String => Unit): Unit = paths.foreach { path => f1(path).foreach { line => f2(line) } }
defined function crunchFiles

@ def printWithIndent(line: String) = println(s"    $line")
defined function printWithIndent

@ crunchFiles(files, getFileContents, printWithIndent)
    apple
    banana
    cat
    dog

@ def printIfWordContainsCharA(line: String) = if (line.contains("a")) println(line)
defined function printIfWordContainsCharA

@ crunchFiles(files, getFileContents, printIfWordContainsCharA)
apple
banana
cat

@ import java.io.File
import java.io.File

@ new File("/tmp").listFiles.filter(_.toString.endsWith("txt"))
res23: Array[File] = Array(/tmp/animals.txt, /tmp/fruits.txt)

@ def crunchFiles(files: Iterator[String], f1: String => Iterator[String], f2: String => Unit): Unit = files.foreach { path => f1(path).foreach { line => f2(line) } }
defined function crunchFiles

@ val listTextFilesUnderDirectory = (dir: String) => new File(dir).listFiles.filter(_.toString.endsWith("txt")).toList.map(_.toString).toIterator
listTextFilesUnderDirectory: String => Iterator[String] = ammonite.$sess.cmd41$$$Lambda$2447/934433293@4f559d04

@ crunchFiles(listTextFilesUnderDirectory("/tmp"), getFileContents, printIfWordContainsCharA)
cat
apple
banana

@
def crunchFiles(dir: String, f0: String => Iterator[String], f1: String => Iterator[String], f2: String => Unit): Unit = f0(dir).foreach { path => f1(path).foreach { line => f2(line) } }
defined function crunchFiles

@ crunchFiles("/tmp", listTextFilesUnderDirectory, getFileContents, printIfWordContainsCharA)
cat
apple
banana

Ammonite ではじめるすくすく Scala 生活

だいたい Ammonite に書いてある。Magic Imports はとてもありがたい。

インストール

$ sudo curl -L -o /usr/local/bin/amm https://git.io/vdNv2 && sudo chmod +x /usr/local/bin/amm && amm

REPL として使う

$ amm
(... snip the greeting message ...)

@  "This is Ammonite REPL".split(" ").foreach(println)
This
is
Ammonite
REPL

REPL として使う + 外部ライブラリを追加する

$ amm
(... snip the greeting message ...)

@ import $ivy.`com.github.nscala-time::nscala-time:2.18.0`, com.github.nscala_time.time.Imports._
import $ivy.$                                           , com.github.nscala_time.time.Imports._

@ DateTime.now().toMutableDateTimeISO
res14: org.joda.time.MutableDateTime = 2018-01-18T20:53:19.347+09:00

REPL として使う + 外部ライブラリを起動時に読み込ませるように設定しておく

$ vi ~/.ammonite/predef.sc
import $ivy.`org.scalaz::scalaz-core:7.2.7`, scalaz._, Scalaz._
import $ivy.`com.github.nscala-time::nscala-time:2.18.0`, com.github.nscala_time.time.Imports._
$ amm
(... snip the greeting message ...)

@ (1.hour + 2.minutes + 34.seconds).getClass
res9: Class[T] = class com.github.nscala_time.time.DurationBuilder

コマンドとして使う(-c)

$ amm -c 'import scala.io.Source; val f = Source.fromFile("/home/kyagi/.gitconfig"); for(l <- f.getLines) { if(l.contains("checkout")) println(l); }'
Compiling /home/kyagi/bin/(console)
  cb = checkout -b
  co = checkout
  tg = "!f() { git checkout -b "$1" "refs/tags/$1"; }; f"
  yo = "!f() { git checkout -b "$1" "origin/$1"; }; f"
  yp = "!f() { git checkout -b "$2" "$1/$2"; }; f"

スクリプトを実行するインタプリンタとして使う (-s はサイレントオプション)

import scala.io.Source
import ammonite.ops._

@main
def main(str: String, path: Path): Unit = {
  val file = Source.fromFile(path.toString)
  for (line <- file.getLines) {
    if (line.contains(str)) println(line)
  }
}
$ amm -s MyGrep.sc branch /home/kyagi/.gitconfig
Compiling /home/kyagi/bin/MyGrep.sc
    ba = branch -ra
    bd = branch -d
    bD = branch -D
    br = branch
    bv = branch -vv

スクリプトを実行するインタプリンタとして使う + 別窓でスクリプトを修正したら自動実行

$ amm -s -w MyHello.sc
hello, wolrd
Watching for changes to 2 files... (Ctrl-C to exit) # 別窓で vi MyHello.sc して s/world/scala/ する
hello, scala
Watching for changes to 2 files... (Ctrl-C to exit)
...

-w オプションは sbt の ~run に相当する(Ruby でいう Guard)。٩(๑´3`๑)۶

Ammonite を使用してお手軽に Scala のスクリプトを書く

sbt は優秀なツールだけれども、さくっと Scalaスクリプトを書くには Ammonite のほうがお気に入り。シェルからは -c オプションで気軽に Scalaスクリプトとして実行可能。

$ amm -c 'import scala.io.Source; val file = Source.fromFile("/home/kyagi/.gitconfig"); file.getLines.foreach(line => { if (line.contains("branch")) println(line) })'
Compiling /home/kyagi/(console)
    ba = branch -ra
    bd = branch -d
    bD = branch -D
    br = branch
    bv = branch -vv

スクリプトをファイルに落として起きたい時は @main というアンモナイト記法? の後に main function を書く。

showGitBranchAlias.sc

import scala.io.Source

@main
def main(): Unit = {
  val file = Source.fromFile("/home/kyagi/.gitconfig")
  file.getLines.foreach(line => {
    if (line.contains("branch")) println(line)
    }
  )
}
$ amm showGitBranchAlias.sc
Compiling /home/kyagi/bin/showGitBranchAlias.sc
    ba = branch -ra
    bd = branch -d
    bD = branch -D
    br = branch
    bv = branch -vv

もちろん REPL としても使える。sbt console よりも早く立ち上がる。プロンプトが @ なのがアンモナイトの名前の由来? ぽい (^.^;

シングルトンオブジェクトを IntelliJ の Scala Worksheet で確認する

Singleton objects are sort of a shorthand for defining a single-use class, which can’t directly be instantiated, and a val member at the point of definition of the object, with the same name. Indeed, like vals, singleton objects can be defined as members of a trait or class, though this is atypical. (TOUR OF SCALA, SINGLETON OBJECTS)

シングルトンオブジェクトは1回しか使われないクラスを便利に定義するための省略表現のようなものである。シングルトンオブジェクトはインスタンス化できない。object キーワードで定義した時点でシングルトンオブジェクトはクラス定義と同時にクラスと同じ名前の val (代入不可) メンバを生成する。変則的な使い方としては、シングルトンオブジェクトは trait や class のメンバとしても利用できる。

Singleton Objects | Scala Documentation

IntelliJ の Scala Worksheet を使って class 定義を継承した object を作ってみた例。object と同じ名前の FooCompany というクラスが同時に定義されているのが getClass から確認できる。

f:id:kyagi:20180116235057p:plain

「シングルトン」の名前の通り、同じ名前のオブジェクトを二回定義することはできない。

f:id:kyagi:20180116235424p:plain

Safari Books Online で他の Scala 本を読んでいると以下のような説明も見つかった。

The object keyword creates a new singleton type, which is like a class that only has a single named instance. (Chapter2 2. Getting started with functional programming in Scala, Functional Programming in Scala)

シングルトンオブジェクトとはインスタンスを一つしか持たないクラスのようなものである。

個人的に PiS3 は読み物として少し難しく感じるため、Safari Books Online で他の Scala 本を理解の補足として読むのが習慣になっている。Funnctional Programming in Scala, Scala for the Impatinet, Pragmatic Scala を並行して読んでいる。(。・ω・。)

読み解き PiS3: ケースシーケンスの実体は関数リテラルである

A sequence of cases (i.e., alternatives) in curly braces can be used anywhere a function literal can be used.Essentially, a case sequence is a function literal, only more general. Instead of having a single entry point and list of parameters, a case sequence has multiple entry points, each withtheir own list of parameters. Each case is an entry point tothe function, and the parameters are specified with the pattern. Thebody of each entry point is the right-hand side of the case. (Chapter 15. Case Classes and Pattern Matching)

  • 偶数判定の関数リテラル(function literals) を作ってみる。Lambda が返っているので無名関数と言ってもよいのかなと思う(Lisp 好きな私には lambda は懐かしい)
scala> val isEven = (x: Int) => { if((x % 2) == 0) true else false }
isEven: Int => Boolean = $$Lambda$3888/394887728@36f14bca

scala> isEven(3)
res10: Boolean = false

scala> isEven(4)
res11: Boolean = true
  • もうちょっと短くしてこれでも大丈夫だった。
scala> val isEven = (x: Int) => (x % 2) == 0
isEven: Int => Boolean = $$Lambda$4044/817940743@616ae52
  • ケースシーケンス中に関数リテラルを埋め込んでみる。
scala> val isEven: Int => Boolean = {
     |   case x: Int => (x % 2) == 0
     | }
isEven: Int => Boolean = $$Lambda$4043/1380981831@d5d3ee

scala> isEven(10)
res49: Boolean = true

scala> isEven(11)
res50: Boolean = false
  • ケースシーケンスを val にひもづかせる時の値としての関数(function values)の書き方は少し特殊だが以下のようだ。PiS3 に説明の記述が見当たらなかった。|ω・)
val variable_name: argument_type => return_type = { ... body ... } 

 読み解き PiS3: ケースクラスのパターンマッチング(Constructor パターン)

  • case class は abstract class の派生でなくてもいい。コップ本だと Expr の派生クラスとして UnOp や BinOp クラスを定義しているが別に親がいなくてもいい。
  • パターンマッチのブロックの中の e はパターンに束縛される一時変数。「この e はどこから出てきたんだ」と思ってしまった。これは慣れるしかない。

In this example, note that the first three alternatives evaluate to e, a variable that is bound withinthe associated pattern. (Chapter 15. Case Classes and Pattern Matching)

  • パターンは以下の種類がある。
    • Wildcards, Constant, Variable, Constructor, Sequence, Tupple, Typed
  • Expr の例はパターンマッチの Constructor パターンに該当する。自分でも Constructor パターンを試してみる。
case class Room(number: Int, kind: String)

val r1 = Room(1001, "single")
val r2 = Room(2001, "double")

def tellMeRoomKind(r: Room): Unit = {
  r match {
    case Room(n, "single") => println(s"Room $n is a single room.")
    case Room(n, "double") => println(s"Room $n is a double room.")
    case Room(n, _) => println(s"Room $n is unknown-kind room.")
  }
}

tellMeRoomKind(r1)
  • パターンマッチも慣れてくれば「これはこのパターンだな」と認識できて、ぐっと表現の幅が広がりそう。Scala は文脈によっていろいろな意味を持たせられるようだ。慣れていないと簡潔な表現を読み解くのが大変な反面、慣れてしまえばその簡潔さゆえに、読み書きともかなりスピードがあがるはず。 (´͈ ᗨ `͈ )

読み解き PiS3: def の後のイコールのありなし

  • def の後の body の前に = を置かない場合と置く場合の違いが気になっていた。
    • = を置かない場合はメソッド/関数の戻り値は Unit になる。
    • = を置く場合はメソッド/関数の戻り値を任意に指定する。

The equals sign that precedes the body of a function hints that in the functional world view, a function defines an expression that results in a value.The basic structure of a function is illustrated in Figure 2.1. (Chapter 2. First Steps in Scala)

  • 自分で sbt console から試してみる。
scala> def hello(s: String) { println(s"hello, $s.") }

hello: (s: String)Unit

scala> hello("world")
hello, world.
scala> def hello(s: String) = { println(s"hello, $s.") }
hello: (s: String)Unit

scala> hello("world")
hello, world.
scala> def hello(s: String): Unit = { println(s"hello, $s.") }

hello: (s: String)Unit

scala> hello("world")
hello, world.
scala> def hello(s: String): String = { println(s"hello, $s.") }
<console>:11: error: type mismatch;
found : Unit
required: String
def hello(s: String): String = { println(s"hello, $s.") }
^
scala> def hello(s: String): String = { "hello, " + s + "." }
hello: (s: String)String

scala> hello("world")
res5: String = hello, world.
  • 同じ疑問を持っている方が説明してくれていて助かった。( ・∀・)

blog.jessitron.com