独学大学情報学部

ただのノート。主にプログラミング。

Monocleとかいうのがありまして

この記事はScala Advent Calendar 2014の22日目です。
日付変わっちゃいました、すみません。

今回はちょっとMonocle触ってみました。

Monocleとは

Julien Truffaut氏がメインで開発してるScalaでLensなライブラリです。

最新安定版はv1.0.1(2014.12.22 現在)で、つい先日リリースされたばかりのピチピチです。

もともとはHaskellのLensパッケージがあって、それをScalaで実装してみたみたいな感じです。 MonocleではLens, Traversal, Optional, Prism, Isoを提供していて、今回はその中からLensとPrismを紹介します。

あと、Lensに関しては圏論的なバックグラウンドがあるそうですが全然詳しくないのでこの記事では触れないことにします。

Lensはどういう時に欲しくなるのか

LensはJavaなどで言うgetterやsetterを抽象化した概念で、不変性を保ちつつネストしたデータ構造に対するアクセスをLensの合成で表現できるようにしたものになります。どういう事か見ていきましょう。

公式READMEのサンプルをちょいとお借りして、下記のようなネストしたデータ構造があったとします。

case class Street(name: String)
case class Address(street: Street)
case class Company(address: Address)
case class Employee(company: Company)

さて、Streetのnameを不変性を保ったまま書き換えたいときにどういったアプローチがあるでしょうか? いろいろな方法があると思いますが、case classであればcopyメソッドを使うのがいいでしょう。

val employee = Employee(Company(Address(Street("chuodori"))))

// Streetのnameをcapitalizeするっ
employee.copy(
  company = employee.company.copy(
    address = employee.company.address.copy(
      street = employee.company.address.street.copy(
        name = employee.company.address.street.name.capitalize
      )
    )
  )
)

おっと、不変性を保ったばかりにとても冗長なコードになってしまいました。 ちなみにvarだった場合はご想像の通りさっきのコードに比べてすっきりかけます。

// もし、変数がvarで再代入可能だったら...
case class Street(var name: String)
case class Address(var street: Street)
case class Company(var address: Address)
case class Employee(var company: Company)

val employee = Employee(Company(Address(Street("chuodori"))))

val capitalizedName = employee.company.address.street.name.capitalize
employee.company.address.street.name = capitalizedName

scala> employee
res0: Employee = Employee(Company(Address(Street(Chuodori))))

ちょいと長いので二行に分けちゃいましたが、copyメソッドを使ってた時よりも遥かにすっきり直感的です。

あぁ...これでは「varで良くね?」って言われても返す言葉がありません.....。不変性を保ちつつ上記varの時と同様にすっきりかけないものでしょうか.....。

そう!ここでLensの出番です!!

MonocleでLens

まずは、対象のデータに対してLensを定義してやる必要があります。
MonocleでのLensの定義方法は以下の3種類です。

  1. 手動で明示的に定義する
  2. Lenser マクロを使う
  3. @Lenses アノテーションを使う

ではそれぞれ試してみましょう。 ※Lensの定義はREPL上ではできないのでsbtにMonocleの依存追加してcompileしてやります。*1

1. 手動で明示的に定義する

import monocle.Lens

object SampleLens {

  // 対象のデータに対するgetterとsetterを引数に渡します。
  val _name = Lens[Street, String](_.name)(str => s => s.copy(name = str))
  val _street = Lens[Address, Street](_.street)(s => a => a.copy(street = s))
  val _address = Lens[Company, Address](_.address)(a => c => c.copy(address = a))
  val _company = Lens[Employee, Company](_.company)(c => e => e.copy(company = c))

}

さぁ、定義ができたら'sbt console'でREPLを立ち上げて実行してみましょう。

scala> import SampleLens._
import SampleLens._

scala> val employee = Employee(Company(Address(Street("chuodori"))))
employee: sample.SampleLens.Employee = Employee(Company(Address(Street(chuodori))))

// 値取得
scala> (_company composeLens _address composeLens _street composeLens _name) get employee
res1: String = chuodori

// 値書き換え
scala> (_company composeLens _address composeLens _street composeLens _name).set("hogedori")(employee)
res2: sample.SampleLens.Employee = Employee(Company(Address(Street(hogedori))))

// 関数の適用(capitalize)
scala> (_company composeLens _address composeLens _street composeLens _name).modify(_.capitalize)(employee)
res3: sample.SampleLens.Employee = Employee(Company(Address(Street(Chuodori))))

これが冒頭で言っていた「データに対するアクセスをLensの合成で表現できる」って話です。 ちなみに「composeLens」のエイリアスとして「^|->」が定義されているので7文字分すっきりできます。

scala> (_company ^|-> _address ^|-> _street ^|-> _name).modify(_.capitalize)(employee)
res5: sample.SampleLens.Employee = Employee(Company(Address(Street(Chuodori))))

不変性を保ちつつ、すっきり書けるようになりました!

2. Lenserマクロを使う

v0.5.1より追加された機能です。 手動で定義するよりも簡単にLensを定義してやることができます。

import monocle.macro.Lenser

object SampleLenserMacro {

  val lenser = Lenser[Employee]

  val _name = lenser(_.name)
  val _street = lenser(_.street)
  val _address = lenser(_.address)
  val _company = lenser(_.company)

}

使い方は1. 手動で明示的に定義した場合と同じなので省略します。

3. @Lensesアノテーションを使う

もっとも簡単にLensを「生成」してやる方法です。 case classの宣言に対して@Lensesアノテーションをつけると各変数に対してLensが定義されます。やばいです。

import monocle.macro.Lenses

object SampleLensesMacro {

  @Lenses
  case class Street(name: String)

  @Lenses
  case class Address(street: Street)

  @Lenses
  case class Company(address: Address)

  @Lenses
  case class Employee(company: Company)

}

ただし、この方法を使うには色々注意が必要です。 内部的にはマクロパラダイスマクロアノテーションを使っていて、プラグインの追加が必要だったり、そもそもcase classの定義に手を加える必要があるので、既に定義済みのcase class(ライブラリの方で定義されてるものなど)に使えなかったりします。 一応、実験的な機能と捉える方がいいでしょう。

また、基本的な使い方は1, 2と同じですが、Lensの生成先がコンパニオンオブジェクト内なのでちょっと面倒になります。

scala> import SampleLensesMacro._
import SampleLensesMacro._

scala> import Street._, Address._, Company._, Employee._
import Street._
import Address._
import Company._
import Employee._

scala> val employee = Employee(Company(Address(Street("chuodori"))))
employee: sample.SampleLenses.Employee = Employee(Company(Address(Street(chuodori))))

scala> (company ^|-> address ^|-> street ^|-> name).modify(_.capitalize)(employee)
res5: sample.SampleLenses.Employee = Employee(Company(Address(Street(Chuodori))))

Prismもあるんだよ?

さて、冒頭で述べましたが、Monocleが提供してるのはLensだけではありません。 Prismもその一つです。Prismはざっくりイメージで言うと「失敗を表現できるようになったLens」です。*2

実際のコード見てもらった方が早いですね。 Lensの時と同様にPrismの定義はREPL上ではできないのでsbtにMonocleの依存追加してcompileしてやります。*3

import monocle.Prism
import scalaz.Maybe

object SamplePrism {

  val strToInt: Prism[String, Int] = 
    Prism { str: String => Maybe.fromTryCatchNonFatal(str.toInt) }(_.toString)
}

'sbt console'でREPLを立ち上げて試してみましょう。

scala> import SamplePrism._
import SamplePrism._

scala> strToInt getMaybe ("1")
res1: scalaz.Maybe[Int] = Just(1)

scala> strToInt getMaybe ("")
res2: scalaz.Maybe[Int] = Empty()

res2でtoIntに失敗した場合はEmpty()が帰ってきているのがわかります。
ちなみに、以下のような使い方もできます。

scala> strToInt.reverseGet(1)
res12: String = 1

scala> strToInt.modify(_ + 100)("1")
res13: String = 101

scala> strToInt.modify(_ + 100)("")
res14: String = ""

reverseGetでInt => Stringをさせることができます。*4
res13は内部的には"1"をIntに変換した後100を足して再度文字列にしています。何も知らずに見ると結構やばいですね。 なお、今回自分で上記のstrToIntを定義しましたが、Scalaの基本型に対するPrismは既にMonocle側で定義されてたりします。

まとめとか

この記事を書き上げる前にMonocle v1.0.1がリリースされました。 自分が触りだした時はv0.5.1でしたが、絶賛開発中の1.x系と比べると引数の順序とかpackage構成とか結構違っていて、 その辺のハマりどころとか書いて「もうすぐリリースされるはずなので、1.x系の安定版を待ちましょう!!」みたいな感じで締めようと思ってたんですけど、そんな必要なくなっちゃいました。安心してv1.0.1を使いましょう!!

とかいいつつ、まだまだ全然全く本当の美味さを把握できてる気がしないので引き続き試してみたいです、はい。

あ、あとargonautの中でも使われていて面白そうなんですけど、全然追えてないのでまたの機会に。*5

 
 


*1:定義時に「object Lens does not take type parameters.」とかってエラー吐く.....

*2:概念的にはLensとPrismはそれぞれ別な方向へIsoを特化させたものであり、「Lensを拡張したのがPrismである」というわけではありません。とはいえ、Monocle内でIsoをベースにLens, Prismが実装されているのかって言うとそうでもないです。Isoに関しては別の機会に...

*3:strToIntが「monocle.Prism.type does not take parameters」とかってエラー吐く.....

*4:Prism定義時、第二引数に渡した関数が呼ばれてます。

*5:今度こそ.....更新頻度増やすんだ.....

新年明けました

新年あけましておめでとうございます。

4日程度遅延したけど気にしないことにした(
正月気分でgdgdするのもいい加減飽きたので、気分転換に日記でも。

年末

あけおめ!した直後に年末の回想ですよ奥さん。
えぇ、今年も有明開催のバーゲンセールに参加してきました。*1
以下、戦利品の一部。(意味深

f:id:Aoino:20130104010913j:plain

ざーっと眺めてみたけど、面白かったです(小並感
あとでレビューとか考察書いてみようかなーと思いつつ、一方で薄い本の内容まとめ・レビューってどうなんだろうと思ったり。*2

非常に財布と体に優しくないイベントだけど、普段会えない先輩方や話の合う人と交流出来るってのが魅力的なのでついつい。 んで、無事生還帰宅してきたわけですが数年ぶりに年越しまで起きていられませんでした......orz

元旦

友人宅にて飲み会&ゲーム

2日

友人と新春投げ売り品の買出しへ。 回線契約ついでの割引でPC買っちゃおうかなとか思ってた自分が甘かったと反省。 今更ながら、上手くできたシステムですよアレは。

3日

弟と買い物。 流行のオシャレなんてどうでもいいとか思ってたけど、流石にフクヲカイニイクフクガナイは避けたいので今風オシャレ大学生な弟様様様にご指南頂き以下割愛。

4日

なう。

抱負

今年こそ技術書含む本100冊読破!

まとめ

適当さマシマシであっと言う間に三が日終わっちゃいました。 去年は非常に色々な事があったし限界スレスレ低空飛行始まっちゃったけど、現状凄く楽しいのでこのままで上手く行ってくれればなーと。

引き続き自身の知識欲満たしつつ、色んな人と交流出来ればと思います。 今年もどうぞよろしくお願いします。


*1:お客様気分で行くと刺されますよ?

*2:何ていうか、あまり見かけないのでモヤモヤします。ダメって事は無いんだろうけども...

暗黙の型変換(Implicit Conversion)のお話

この記事はPlay or Scala Advent Calendar 2012の18日目です。
何か作ってみるとか言いながら解説記事になりました(謝
総力尽くして色々解読してますが、補足・間違い等ありましたらご指摘ください。

暗黙のパラメータ(Implicit Parameter)についても一緒にまとめようと思ったけど、長くなりそうだったので省きます...。

そもそも何なのこの機能

語弊も承知で言うならばコンパイラが勝手にやってくれる型キャストみたいな機能」 です。こりゃ確実に刺されますね、はい。
もう少しちゃんと説明すると、Scalaコンパイラは型の不一致によるエラーを検出した際に悲鳴をあげる前にスコープ内のimplicitキーワードで定義されたメソッドを用いて解決を図ります。もし、解決出来る場合はそのメソッドを自動的に挿入して実行に移ります。 既存のライブラリ直接弄ることなく拡張できちゃうすんばらしぃ機能です。

種類とか例とか

Implicit Conversionは主に「要求された型への変換」と「レシーバーの変換」の二つに分けられます。

○ 要求された型への変換
求められている型と与える型が不一致だった場合に

// もちろんエラー
scala> val num: Int = "1"
<console>:7: error: type mismatch;
 found   : java.lang.String("1")
 required: Int
       val num: Int = "1"
                      ^

// 暗黙の型変換を定義すると
scala> implicit def string2int(s: String) = s.toInt
string2int: (s: String)Int

scala> val num: Int = "1"
num: Int = 1

ってな感じで暗黙の内に変換してくれます。便利ね!!
まぁ、実際こんな変換定義したら刺される程度じゃ済まn(ry

○ レシーバーの変換
与えられた文字列にわざわざ「Lonely」を付け加えて表示するウザいメソッドlonelyString型に追加したいとすれば

// レシーバーの変換 String -> LonelyString
scala> class LonelyString(str: String) {
     |   def lonely() = println("Lonely " + str + "!!")
     | }
defined class LonelyString

scala> implicit def string2lonelyString(s: String) = new LonelyString(s)
string2lonelyString: (s: String)LonelyString

scala> "Xmas".lonely
Lonely Xmas!!

ということで、あたかもStringlonelyメソッドが存在するかの如く振る舞ってくれちゃいます。凄いね!!

制限・条件

Inplicit Conversionを発動!させるにはもちろん制限・条件があります。

  • implicitキーワードが付けられた定義のみ、暗黙の型変換に利用される
  • 単一の識別子として上記の定義がスコープ内に存在している
  • 暗黙の型変換は一回のみ適用される(型変換の型変換で整合性を取ろうとしない)
  • 明示的変換等によって元々動作する場合、暗黙の型変換は適用されない

ちなみに

上記例のレシーバーの変換について、Scala 2.10では以下の様に書くことが出来ます。

scala> implicit class LonelyString(str: String) {
     |   def lonely() = println("Lonely " + str + "!!")
     | }
defined class LonelyString

scala> "Xmas".lonely
Lonely Xmas!!

これだけでいいの!?キャー簡潔っ///
あぁ...流行に乗り遅れてる感ががが...ちゃんと仕様追いかけます(´・ω・`)

ちょっと探検

だいたいおおよそおおざっぱにお分かり頂けたと思うので、超お馴染みなScalaString型がどのようにして「すげぇRich///」になってるのか見てみましょう。*1
コップ本にもこの話はありますので読むといいかもです。

まず、Scalaコンパイラは拡張子.scalaが付いたファイル全てに以下のインポート文を暗黙のうちに追加しています。

import java.lang._
import scala._
import Predef._

ScalaStringPredefの中に

// scala/src/library/scala/Predef.scala
package scala
// ...
object Predef extends LowPriorityImplicits {
  // ...
  type String = java.lang.String
  // ...
  @inline implicit def augmentString(x: String): StringOps = 
    new StringOps(x)
  // ...
}

の様に定義されています。java.lang.Stringに対するメソッド呼び出しは暗黙のうちにaugmentStringメソッドによって StringOpsに変換されます。StringOpsは数多のメソッドを含んだクラスで、 一通り眺めるだけでお腹一杯になれます。*2

Scala 2.7の頃のString型は現在のWrappedStringへ変換されていました。WrappedStringStringOpsreversefilterメソッドの返り血が異なるくらいで、あとは似たようなものかと。*3
違いを具体的に見るならば

// Scala 2.8以降は直感的に
scala> "abc".reverse.reverse == "abc"
// => true

// WrappedStringを使うと
scala> val str = new scala.collection.immutable.WrappedString("abc")
res0: scala.collection.immutable.WrappedString = abc

scala> str.reverse.reverse == "abc"
// => false

ってな感じでおわかりいただけただろうか(震え声

ただ、Scala 2.8以降はWrappedStringへの暗黙の型変換はPredefの親クラスLowPriorityImplicits

// scala/src/library/scala/LowPriorityImplicits.scala
package scala
// ...
class LowPriorityImplicits {
  // ...
  implicit def wrapString(s: String): WrappedString =
    if (s ne null) new WrappedString(s) else null
  implicit def unwrapString(ws: WrappedString): String =
    if (ws ne null) ws.self else null
  // ...
}

と定義されていますが、コンパイラPredefLowPriorityImplicitsの継承関係より Predefに定義されている型変換を行います。*4
ってことで、普段使う分にはあまり考慮する必要がないってことですね!

まとめ

\\ Scalaすげぇ // \\ Scalaすげぇ // \\ Scalaすげぇ //


*1:Scala Standard Library 2.9.2です。

*2:実際にはStringLikeトレイトまでで大半を実装してるのでStringOpsクラスのソース自体は非常に小さいです。

*3:WrappedStringStringLike[WrappedString]StringOpsStringLike[String]をmixinしています。

*4:複数の型変換が存在する時、コンパイラはサブクラス側の型変換を選択します。

最短でつぶやくぷろぐらむ(

「今世紀最高傑作」とか「ぼくのかんがえたさいきょうの...」並に信用ならんタイトルです。 勢いとキャッチコピーは大切だと思いますぼくぁ(

準備

twitter4jとsbt使います。
Twitter Developersにアクセスして、Consumer key, Consumer secret, あとアクセストークン生成してAccess token, Access token secretをコピってきます。

手順

てけとーにディレクトリ作って、その中にbuild.sbtファイルを以下の内容で作ります。

name := "Quick Post Program"

version := "1.0"

scala := "2.9.2"

libraryDependencies += "org.twitter4j" % "twitter4j-core" % "2.2.6"

で、その後に

sbt update

とかやるとtwitter4jのjarを勝手に落としてきてパス通してくれたりします。 あとはsrc/main/scala/QuickPost.scalaってファイルにて

package main.scala

import twitter4j.conf.ConfigurationBuilder
import twitter4j.TwitterFactory

object QuickPost {
  val CONSUMER_KEY = "Consumer key"
  val CONSUMER_SECRET = "Consumer secret"
  val ACCESS_TOKEN = "Access token"
  val ACCESS_TOKEN_SECRET = "Access token secret"

  def main(args: Array[String]) {
    val builder = new ConfigurationBuilder
    builder.setOAuthConsumerKey(CONSUMER_KEY)
    builder.setOAuthConsumerSecret(CONSUMER_SECRET)
    builder.setOAuthAccessToken(ACCESS_TOKEN)
    builder.setOAuthAccessTokenSecret(ACCESS_TOKEN_SECRET)

    val twitter = new TwitterFactory(builder.build).getInstance

    twitter.updateStatus("Hello, Azusa!")
  }
}

んでもって

sbt run

で完了っと。

まとめ

自分で使うだけならこれでいいと思います。(今更感

tmuxの導入

いちいちマウス使うの面倒になってきたので導入しようかと。
タイル型WM入れても良いと思ったけど、Haskell or Luaで悩んだのでとりあえず保留。

概要

tmuxは単一画面上に複数のターミナルを起動出来るようにする便利なアプリケーションです。 "Terminal multiplexer"って書いた方が分かりやすいでそ(

導入

sudo yum install tmux

でおk。yumは適宜読み替えで。

設定

基本的な設定方法はここ読めばいけると思います。 → Manpage of TMUX
あとは"導入"ってタイトルに恥じないよう、やっておきたい初期設定をいくつか↓

  • 設定ファイル

    デフォルトでは/etc/tmux.confで、存在すれば~/.tmux.conf。
    こだわりとか深刻な事情がなければ~/.tmux.confで良いと思います。
    コメントに説明付けた設定ファイル↓ ※setはset-operationと同義。

      # Prefix keyの変更時に。デフォルトはCtrl+b
      # set -g prefix C-b
    
      # 一応自動検出されるけど、必要であれば明示的に指定。
      #setw -g utf8 on
      #set -g status-utf8 on
    
      # コピーモードのキーバインド。デフォはEmacs(
      set -g mode-keys vi
    
      # 中央のウィンドウリストは色と属性のみ変更可能です。
      # なんとなく秒も表示させたいのでintarvalは1。
      # 以下の設定で左の領域に時間、右の領域にホスト名が表示されます。
      set -g status-interval 1
      set -g status-left-length 19
      set -g status-left "%m/%d(%a) %H:%M:%S"
      set -g status-right "#H"
    

    この他、色の設定とか色の設定もあると思うけど、初期設定ならこれくらいで(

  • 基本操作

    以下を参考に。はい、手抜きです。
    時代はGNU screenからtmuxへ - Dマイナー志向
    tmuxのすすめ | catatsuyのBlog

まとめ

"tmux"って何て発音すればいいんだろう?



※12/12 右ステータスバー設定の誤字を修正("%H" → "#H")