scalaの復習

scalaをかなり久々に使ってみた。文法をすっかり忘れていてReferenceのPDF(のBNF)見ながら書きました。

LISPを作ろうとしたけど、そこまでいたらずパーズしてbuiltin関数を呼べる程度(lambdaもmacroも無い)。

// scalac lisp.scala
// scala lisp.Main

package lisp

import scala.util.matching.Regex
import scala.util.parsing.combinator._

object Main extends JavaTokenParsers {
  abstract class Expr
  case class Sym(symbol: Symbol) extends Expr
  case class Str(string: String) extends Expr
  case class Num(number: Long) extends Expr
  case class Lst(list: List[Expr]) extends Expr

  def symbol: Parser[Expr] = new Regex("""[-a-zA-Z0-9+*/%._]+""") ^^
    ((token: String) => Sym(Symbol(token)))
  def string: Parser[Expr] = stringLiteral ^^
    ((token: String) => Str(lit2str(token)))
  def numeric: Parser[Expr] = decimalNumber ^^
    ((token: String) => Num(token.toLong))
  def list: Parser[Lst] = "(" ~> exp.* <~ ")"  ^^
    ((list: List[Expr]) => Lst(list))
  def exp = string | numeric | symbol | list

  def lit2str(lit: String) = {
    val escape: (Char => Char) = {
        case 'r' => '\r'
        case 'n' => '\n'
        case 't' => '\t'
        case other => other
    }
    val (ret, _) = lit.substring(1, lit.size - 1).foldLeft(("", false)) {
      case ((buf, true), cur) => (buf + escape(cur), false)
      case ((buf, false), '\\') => (buf, true)
      case ((buf, false), cur) => (buf + cur, false)
    }
    ret
  }

  type Env = Map[Symbol, Any]


  def eval(env: Env)(expr: Expr): Any = expr match {
    case Sym(symbol) => env(symbol)
    case Str(string) => string
    case Num(number) => number
    case Lst(list) => {
      val vals: Any = list.map { eval(env)(_) }
      //println(vals)
      vals match {
        case () => ()
        case func :: args =>
          func.asInstanceOf[List[Any] => Any].apply(args)
      }
    }
  }

  val env: Env = Map(
    Symbol("+") ->
      ((args: List[Any]) => args.foldLeft(0L)(
        (a, b) => a + b.asInstanceOf[Long])),
    Symbol("p") ->
      ((args: List[Any]) => { args.foreach(print); println })
  )

  def main(args:Array[String]) {
    val list = parse(exp, """(p "10 + 20 = " (+ 10 20))""")
    //val list = parse(exp, """(+ 10 20)""")
    println(list.get)
    println(eval(env)(list.get))
  }
}


注目点

  • { case PATTERN => ... }でのクロージャが(arg) => arg match { case PATTERN => ...}の省略形である
  • func{...} は func({...})であり、func{...}.xxxはfunc({...}).xxxである
    • ぱっとみの誤解を避けるために括弧をつけたほうがよさそうだ
  • 戻り値が無い(Unit型, (), Javaでのvoid)関数は、=なしでかける。この場合、ブロック式の値が何でも、戻り値Unitになる
  • 部分適用 func(a)(_)はdef foo(a)(b)のように定義しなくてはいけない
  • str.foreach(print)とかける。foreachが関数型をうけとるよう定義されてるからっぽい。def foo(arg:Any)だとfoo(print)とは書けない。foo(print _)のように書く必要がある(またはfoo(print: (Any => Unit)))
  • Mapの引数の a -> bはタプル(a, b)の別記法


ポジティブな感想

  • シンボル
  • パターンマッチ
  • 関数型プログラミングもできる点
  • Javaよりはデフォルトネームスペースで提供する機能が多いとこ(ListやMapなど)
  • パーザー関係機能が標準にあること
  • (メソッド呼び出しを二項演算子風にかけるとこ)
  • (XML構文があるとこ)

ネガティブな感想

  • groovyのほうがいいと感じる点も結構ある
    • リスト、マップ、正規表現リテラルとか、配列風アクセス[]とか
    • (別文化だと大して違わないのかもしれないけど、List(1,2,3)より[1,2,3]のほうがいいと思ってしまう。[]を型パラメータで使ってるのが痛いかも)
  • タプルが便利であるが、それを使うと括弧が多くなりがちに
  • 動的言語と比べてしまうと、リフレクションが弱い
  • 若干混乱する仕様
    • val a, b = 100 は val a = 100; val b = 100 (タプルを展開したいときは val (a, b) = tup)
    • List要素アクセスはl(0),l(1),l(2),l(3)だが、タプル要素アクセスはt._1, t._2, t._3, t._4
    • コマンドscalaとscalacで、scalaコードで書くべき内容の違いがある点

最初のlit2str

  def lit2str(lit: String) =
    lit.substring(1, lit.size - 1).foldLeft(("", false)) {
      case ((buf, esc), cur) => {
        if (esc)
          (cur match {
            case 'r' => buf + '\r'
            case 'n' => buf + '\n'
            case 't' => buf + '\t'
            case _ => buf + cur
          }, false)
        else
          (if (cur == '\\') (buf, true) else (buf + cur, false))
      }
    }._1

ここから上記にリファクタリングしました。

まず、buf + をmatchの外側に出します。そして esc分岐をパターン中に埋め込み、trueのときのescape部分は外に関数として出し、falseのときのバックスラッシュ処理はパターンに埋め込む、という感じを経て上のコードになりました。


関数型だと、ボトムアップで組み立てがちで、探りつつ式をその場で入れ替えながらやってしまうことで、後から見ると複雑になってしまう。でもこうしたちょっとの入れ替えだけでだいぶ見易さが変わるのも、関数型のいいところでしょうか(逆にちょっとの入れ替えで大きく見にくくなるともいえてしまうけど)。