ラベル tips の投稿を表示しています。 すべての投稿を表示
ラベル tips の投稿を表示しています。 すべての投稿を表示

2016年7月31日日曜日

[SDN] Generalized type constraints

Scalaの持つ型機能にGeneralized type constraintsがあります。訳語がよく分からなかったのでここでは型制約と呼びます。

Scalaプログラミングの要諦は、いかにプログラムのバグをコンパイルエラーで検出するか、だとすると型制約はこの目的を進めるために有効な言語機能です。使えるところではきっちりと使っていきたいところです。

そんなこともあり型制約の利用方法の最新状況が気になったので調べてみました。

型制約については先達の素晴らしい解説があります。

調べた範囲では、型制約の利用方法は上記ブログで取り上げている以下の2つにつきるようです。

  • 型によって有効になるメソッド
  • ビルダ

また上記の調査をしている最中に以下の用途もあるかもというのを思いつきました。今の所使用例は見かけていませんが、もしかして有効かもという使い方です。

  • 状態/状態遷移

おさらいも含めて上記3つの使い方について説明します。

型によって有効になるメソッド

「型によって有効になるメソッド」は型制約の基本的な使い方です。型制約が元々想定している利用方法だと思います。

以下では型パラメタTを持ったケースクラスResourceを定義しています。

openFileメソッドは「<:<」による型制約定義でResourceが保持しているリソースがFile(またはFileのサブクラス)だった時のみ有効となります。

case class Resource[T](r: T) {
    def openFile(implicit ev: T <:< File): InputStream = {
      new FileInputStream(r)
    }
  }

この目的で型制約を使うことによって以下の効能があります。

  • よく使われる特定のリソース向けのメソッドを簡単に定義できる。(この機能がないとトレイトの継承といった大掛かりな仕組みを使う必要がある。)
  • 有効でないリソースに対して誤ったメソッドが呼ばれることを防ぐ。
  • リソースの型がFileに定義された状態でメソッドの実装を行うことができる。
コンパイルエラーによる検出

コンパイルエラーとなる例です。

数値100を格納したResourceを作成し、openFileメソッドを呼び出します。

val r = Resource(100)
    val in = r.openFile

このコードは以下のように型チェックでコンパイルエラーとなります。このように誤った使い方をした場合、コンパイルエラーとして検出することができるわけです。

[error] .../Main.scala:45: Cannot prove that Int <:< java.io.File.
[error]     val in = r.openFile
[error]                ^
[error] one error found

ビルダ

型制約の使い方として有名なのがビルダです。

ビルダで必須項目が設定されていない状態でビルドを行うコードはコンパイルエラーになります。

以下のUrlBuilderはjava.net.URL用ビルダの例です。

import java.net.URL
import UrlBuilder._

case class UrlBuilder[HasProtocol <: YesNo, HasHost <: YesNo] private (
  private val _protocol: Option[String] = None,
  private val _host: Option[String] = None,
  private val _port: Option[Int] = None,
  private val _file: Option[String] = None
) {
  def isCompleted = _protocol.isDefined && _host.isDefined

  def protocol(s: String) = new UrlBuilder[Yes, HasHost](
    Some(s), _host, _port, _file)
  def host(s: String) = new UrlBuilder[HasProtocol, Yes](
    _protocol, Some(s), _port, _file)
  def port(s: Int) = copy(_port = Some(s))
  def file(s: String) = copy(_file = Some(s))

  def build(implicit ev1: HasProtocol =:= Yes, ev2: HasHost =:= Yes): URL =
    (_protocol, _host, _port, _file) match {
      case (Some(s), Some(a), Some(p), Some(f)) => new URL(s, a, p, f)
      case (Some(s), Some(a), Some(p), None) => new URL(s, a, p, "")
      case (Some(s), Some(a), None, None) => new URL(s, a, "")
      case (Some(s), Some(a), None, Some(f)) => new URL(s, a, f)
      case _ => throw new IllegalStateException(s"Illegal parameters $this")
    }
}

object UrlBuilder {
  sealed trait YesNo
  sealed trait Yes extends YesNo
  sealed trait No extends YesNo

  def builder = new UrlBuilder[No, No]()
}

型パラメタに設定する目的のトレイトYesNoとそのサブトレイトYes、Noを定義しています。

UrlBuilderは2つの型パラメタHasProtocolとHasHostを定義していて、いずれもトレイトYesNoとそのサブトレイトを型として設定できるようにしています。

ポイントはbuildメソッドで型制約「=:=」を使って型パラメタHasProtocolとHasHostの型をYesに限定しているところです。こうすることで型パラメタHasProtocolまたはHasHostにYesが設定されていない場合はコンパイルエラーとなります。

型パラメタHasProtocolにYesが設定されるのは、protocolメソッドが呼ばれてprotocolが設定された時です。また型パラメタHasHostにYesが設定されるのは、hostメソッドが呼ばれてHostが設定された時です。

つまり必須項目であるプロトコルとホストが設定されていない状態でbuildメソッドを使ってURLを生成しようとするとコンパイルエラーとなるわけです。

使い方

以下のようにprotocolとhostを設定後にbuildメソッドを呼び出すとコンパイルエラーにはなりません。

UrlBuilder.builder.protocol("http").host("example.com").build
コンパイルエラーによる検出

UrlBuilderにパラメタを設定しないでbuildメソッドを呼び出します。

UrlBuilder.builder.build

すると以下のようにコンパイルエラーとなります。

[error] .../Main.scala:16: Cannot prove that sample.UrlBuilder.No =:= sample.UrlBuilder.Yes.
[error]     UrlBuilder.builder.build
[error]                        ^

状態

型制約の使い方として「型によって有効になるメソッド」と「ビルダ」以外に、状態や状態遷移の記述にも使えるのではと思いついたので、そのアイデアの紹介です。

状態や状態遷移の操作に対するバグをコンパイル時に検出できればメリットは非常に大きいと思います。ただ、イベント駆動プログラムのような動的な処理で使用できる範囲は狭いと思われるので、ぴったりフィットするユースケースがあるかは将来課題です。

GreetingServiceは、指定された範囲に挨拶メッセージを送るサービスです。

以下の2つの状態を持ちます。

  • Openされているか否か
  • Assignされているか否か

GreetingServiceは全世界にメッセージを送る場合は、大量配信になるため復数のリクエスをと同時実行できないので、事前にリソースをアサインする必要があるという設定です。

import java.net.URL
import GreetingService._

case class GreetingService[IsOpened <: YesNo, IsAssigned <: YesNo] private (private val _url: URL) {
  def open()(implicit ev1: IsOpened =:= No, ev2: IsAssigned =:= No) = new GreetingService[Yes, IsAssigned](_url)
  def close()(implicit ev: IsOpened =:= Yes) = new GreetingService[No, IsAssigned](_url)
  def assign()(implicit ev1: IsOpened =:= Yes, ev2: IsAssigned =:= No) = new GreetingService[IsOpened, Yes](_url)
  def release()(implicit ev1: IsOpened =:= Yes, ev2: IsAssigned =:= Yes) = new GreetingService[IsOpened, No](_url)

  def helloLocalArea(msg: String)(implicit ev: IsOpened =:= Yes) {
    // do send to local area
  }

  def helloWorldWide(msg: String)(implicit ev1: IsOpened =:= Yes, ev2: IsAssigned =:= Yes) {
    // do send to world wide
  }
}

object GreetingService {
  sealed trait YesNo
  sealed trait Yes extends YesNo
  sealed trait No extends YesNo

  def create(url: URL) = new GreetingService[No, No](url)
}

上記2つの状態を型パラメタIsOpenedとIsAssingedで記述します。

型パラメタに設定する型はビルダで使用したYesNo, Yes, Noと同じ方式のものです。

GreetingServiceのポイントはopen, close, assign, releaseの各メソッドが「=:=」による型制約定義でGreetingServiceの状態によって以下の制約を持っていることです。

open
オープンもアサインのされていない時のみ可
close
オープン済みの時のみ可
assign
オープン済みで未アサインの時のみ可
release
オープン済み、アサイン済みの時のみ可

closeメソッドはエラー処理などでアサイン状態にかかわらずクローズしたいケースを想定してアサインの制約は設定していません。

使い方

GreetingServiceは以下のようにして使います。

val url = UrlBuilder.builder.protocol("http").host("example.com").build
    val srv = GreetingService.create(url)
    val opened = srv.open()
    opened.helloLocalArea("local")
    val assigned = opened.assign()
    assigned.helloLocalArea("local")
    assigned.helloWorldWide("world")

openメソッドでオープンした後のGreetingServiceではhelloLocalAreaメソッドでローカル向けのメッセージ送信ができます。

assignメソッドでアサインした後のGreetingServiceではhelloLocalAreaメソッドでのローカル向けのメッセージ送信に加えて、helloWorldWideメソッドで全世界向けのメッセージ送信が可能になっています。

コンパイルエラーによる検出

誤った使用をコンパイルエラーで検出できるか見ていきます。

まずオープン前のGreetingServiceでメッセージを送る場合です。

val url = UrlBuilder.builder.protocol("http").host("example.com").build
    val srv = GreetingService.create(url)
    srv.helloLocalArea("local")

無事helloLocalAreaメソッドがコンパイルエラーになりました。

[error] .../Main.scala:50: Cannot prove that sample.GreetingService.No =:= sample.GreetingService.Yes.
[error]     srv.helloLocalArea("local")
[error]                       ^

次にオープン済みで未アサインの場合に全世界向けメッセージ送信する場合です。

val url = UrlBuilder.builder.protocol("http").host("example.com").build
    val srv = GreetingService.create(url)
    val opened = srv.open()
    opened.helloLocalArea("local")
    opened.helloWorldWide("world")

こちらも無事helloWorldWideメソッドがコンパイルエラーになりました。

[error] .../Main.scala:58: Cannot prove that sample.GreetingService.No =:= sample.GreetingService.Yes.
[error]     opened.helloWorldWide("world")
[error]                          ^
ユーティリティメソッド

GreetingServiceを引数にするユーティリティメソッドでも型パラメタのチェックを行うことができます。型チェックをしたくない項目は「_」にしておけばよいようです。

def sendToLocalAreal(srv: GreetingService[Yes, _]) {
    srv.helloLocalArea("local")
  }

  def sendToWorldWide(srv: GreetingService[Yes, Yes]) {
    srv.helloWorldWide("world")
  }

使い方は以下になります。

val url = UrlBuilder.builder.protocol("http").host("example.com").build
    val srv = GreetingService.create(url)
    val opened = srv.open()
    sendToLocalAreal(opened)
    val assigned = opened.assign()
    sendToWorldWide(assigned)

状態遷移

「状態」の発展形です。

表現できる範囲は限定的ですが、状態遷移を型パラメタを使って実現することも可能です。

以下のGreetingServiceは前述のGreetingServiceと機能は同じですが、型制約の実現方法を変更しています。

具体的には、GreetingServiceは未オープン⇔オープン済⇔アサイン済の階層構造で状態遷移を行うので、この状態遷移の構造を反映した型を使用します。

import java.net.URL
import GreetingService._

case class GreetingService[+State <: OpenState] private (private val _url: URL) {
  def open()(implicit ev: State <:< Closed) = new GreetingService[Opened](_url)
  def close()(implicit ev: State <:< Opened) = new GreetingService[Closed](_url)
  def assign()(implicit ev: State <:< Opened) = new GreetingService[Assigned](_url)
  def release()(implicit ev: State <:< Assigned) = new GreetingService[Opened](_url)

  def helloLocalArea(msg: String)(implicit ev: State <:< Opened) {
    // do send to local area
  }

  def helloWorldWide(msg: String)(implicit ev1: State <:< Assigned) {
    // do send to world wide
  }
}

object GreetingService {
  sealed trait OpenState
  sealed trait Closed extends OpenState
  sealed trait Opened extends OpenState
  sealed trait Assigned extends Opened

  def create(url: URL) = new GreetingService[Closed](url)
}

まず型制約の用の型としてOpenStateトレイトを定義し、このサブトレイトとしてClosedトレイトとOpenedトレイトを、OpenedトレイトのサブトレイトとしてAssignedトレイトを定義しました。

OpenedトレイトとAssignedトレイトのサブトレイト関係で状態遷移の階層構造を表現しています。

OpenState, Opened, Assignedのトレイトを使用してopen, close, assign, releaseの各メソッドが「<:<」による型制約定義によりGreetingServiceの状態によって以下の制約を課しています。この例ではトレイト間のサブクラス関係も利用するので「=:=」ではなく「<:<」を使用しています。

open
Closedの時のみ使用可(オープンもアサインのされていない時のみ可)
close
Openedの時のみ使用可(オープン済みの時のみ可)
assign
Openedの時のみ使用可(オープン済みの時のみ可)
release
Assginedの時のみ使用可(オープン済み、アサイン済みの時のみ可)

前出の「状態」との違いはassignメソッドの制約条件が若干違うことですが、基本的には同等のものとなっています。

使い方

GreetingServiceの使い方は前出の「状態」のものと同じです。

val url = sample.UrlBuilder.builder.protocol("http").host("example.com").build
    val srv = GreetingService.create(url)
    val opened = srv.open()
    opened.helloLocalArea("local")
    val assigned = opened.assign()
    assigned.helloLocalArea("local")
    assigned.helloWorldWide("world")
コンパイルエラーによる検出

コンパイルエラーによるエラー検出も前出の「状態」のもの基本的には同じになります。

まずオープン前のGreetingServiceでメッセージを送る場合です。

val url = sample.UrlBuilder.builder.protocol("http").host("example.com").build
    val srv = GreetingService.create(url)
    srv.helloLocalArea("local")

無事helloLocalAreaメソッドがコンパイルエラーになりました。

[error] .../Main.scala:36: Cannot prove that sample2.GreetingService.Closed <:< sample2.GreetingService.Opened.
[error]     srv.helloLocalArea("local")
[error]                       ^

次にオープン済みで未アサインの場合に全世界向けメッセージ送信する場合です。

val url = sample.UrlBuilder.builder.protocol("http").host("example.com").build
    val srv = GreetingService.create(url)
    val opened = srv.open()
    opened.helloLocalArea("local")
    opened.helloWorldWide("world")

こちらも無事helloWorldWideメソッドがコンパイルエラーになりました。

[error] .../Main.scala:44: Cannot prove that sample2.GreetingService.Opened <:< sample2.GreetingService.Assigned.
[error]     opened.helloWorldWide("world")
[error]                          ^

まとめ

Generalized type constraintsはかなり以前(2.8)からある機能ですが、新しい使い方などが登場していないかという確認の意味もあり利用方法について調べてみました。

特に新たしい使用方法は見つけることはできませんでしたが、調べている中で「状態/状態遷移」の実装方法について思いついたことがあったので形にしてみました。うまくするとフィットする利用方法が見つかるかもしれません。

Generalized type constraintsについては、当初は暗黙変換も対象にする型制約「<%<」があったのですが、最近の版では使えなくなっているようです。この型制約があると「型によって有効になるメソッド」の応用で色々と技が使えそうだったのですが、色々負担の大きそうな機能なのでいたしかたなさそうです。

いずれにしてもGeneralized type constraintsが強力な機能であることを再認識しました。使えそうなポイントを見つけて製品開発にも適用していきたいと思います。

諸元

  • Java 1.7.0_75
  • Scala 2.11.7

2016年4月30日土曜日

型安全イコール判定 - ScalazとScalactic

Scalaプログラミングのはまりポイントとして頻出頻度が高く影響も甚大なのは、==メソッドとcontainsメソッドの型チェックだと思います。

この2つのメソッドは(多分Javaとの互換性の問題で)、引数に定義されている型がAnyであるため事実上型チェックが効かない仕様になっています。

具体的には以下のような処理がコンパイルエラーにならず通ってしまうという問題です。

scala> "1" == 1
"1" == 1
res112: Boolean = false

scala> List(1, 2, 3).contains("1")
List(1, 2, 3).contains("1")
res113: Boolean = false

いずれの場合も、常に判定結果はfalseになり、意図した処理でないことは明らかですがScalaコンパイラはエラーとして弾いてくれません。

Scalaは型チェックが厳しいので、きちんとプログラミングしていればケアレスミス的なバグはほとんどでないのですが、逆に出る場合は==メソッドとcontainsメソッドのあたりに集中するというのがボクの実感です。

例題

==メソッドとcontainsメソッドの問題がよく発生する例として、クラスで保持しているプロパティの型がOptionのケースがあります。

以下の例ではcityの型がOption[String]となっています。

case class Person(name: String, city: Option[String])

特によくバグになるパターンとしては最初に:

case class Person(name: String, city: String)

として(Optionなしの)String型で定義していたものを、機能追加などでOption[String]に変更した場合です。このような場合には、コンパイルエラーで影響箇所が検出されることを期待しているわけですが、==メソッドとcontainsメソッドを使っている場所はエラーにならず、ロジック的に常にfalseとなるため、即バグになってしまいます。

準備

説明では以下のクラスとデータを使用します。

case class Person(name: String, city: Option[String])
  val cities = Vector("Yokohama", "Kawasaki", "Kamakura")
  val person = Person("Taro", Some("Yokohama"))
  val persons = Vector(
    Person("Taro", Some("Yokohama")),
    Person("Jiro", Some("Kawasaki")))
ScalazとScalactic

==メソッドとcontainsメソッドの問題に対応するための機能を提供するライブラリとしてScalazとScalacticを使ってみます。

ScalazはMonadicプログラミング向けのライブラリの1機能(型クラスEqual)として==メソッド問題の解を提供しています。

一方Scalacticは、Scalatestのスピンオフ機能で==メソッド問題を中心にオブジェクト操作の便利機能を提供するライブラリです。

ダメロジック

ダメロジックとして==メソッドとcontainsメソッドの例を順にみていきます。

==メソッド

Personのcityプロパティの方はOption[String]なのでStringと比較しても意味がないのですが、以下のように==メソッドでは比較できてしまいます。

person.city == "Yokohama"

コンパイルは通りますが、実行結果は必ずfalseになってしまいます。

containsメソッド

Personの集まりからcitiesに登録された市に所属する人を抽出する処理です。

persons.filter(x => cities.contains(x.city))

本来比較できないPersonのcityプロパティのOption[String]と、citiesに入っているStringを比較していますが、コンパイルエラーになりません。

しかし、containsメソッドの結果は必ずfalseになるため、全体の実行結果は必ず空の集まりが返ってきてしまいます。

Scalazによる解

まずScalazの提供する機能を使ってみます。

準備として以下のimportを行います。

import scalaz._, Scalaz._
==メソッド

Scalazでは、型チェックあり版の==メソッドとして===メソッドを提供しています。

エラー検出

オブジェクトの同定比較として==メソッドの代わりに===メソッドを用いると型チェックしてエラー検出するようになります。

person.city === "Yokohama"
[error] .../src/main/scala/sample/Main.scala:66: type mismatch;
[error]  found   : String("Yokohama")
[error]  required: Option[String]
[error]       person.city === "Yokohama"
[error]                       ^

簡単に使えて効果抜群です。

対応

コンパイルエラーに対する対応は、比較の型を合わせる修正です。

person.city === Some("Yokohama")
containsメソッド

Scalazでは、型チェックあり版のcontainsメソッドとしてelementメソッドを提供しています。

エラー検出

オブジェクトの集まりでの存在確認にcontainsメソッドの代わりにelementメソッドを用いると型チェックしてエラー検出するようになります。

persons.filter(x => cities.element(x.city))
[error] .../src/main/scala/sample/Main.scala:67: type mismatch;
[error]  found   : Option[String]
[error]  required: String
[error]       persons.filter(x => cities.element(x.city))
[error]                                            ^

こちらも簡単に使えて効果抜群です。

対応

コンパイルエラーに対する対応は、比較の型を合わせる修正です。以下の例では、Optionのfoldメソッドを使ってみました。

persons.filter(_.city.fold(false)(cities.element))

Scalacticによる解

Scalacticで同定比較によるコンパイラエラー抽出を行うためには以下のimportを行います。

import scalactic._
import TypeCheckedTripleEquals._

「import TypeCheckedTripleEquals._」を他のものに変えると、同定比較の方式を変更することができます。

==メソッド

ScalacticではScalazと同様に同定比較によるコンパイラエラー抽出用に型チェックあり版の==メソッドとして===メソッドを提供しています。

エラー検出

オブジェクトの同定比較として==メソッドの代わりに===メソッドを用いると型チェックしてエラー検出するようになります。

person.city === "Yokohama"
[error] .../src/main/scala/sample/Main.scala:28: types Option[String] and String do not adhere to the type constraint selected for the === and !== operators; the missing implicit parameter is of type org.scalactic.CanEqual[Option[String],String]
[error]     person.city === "Yokohama"
[error]                 ^

こちらも簡単に使えて効果抜群です。

対応

コンパイルエラーに対する対応は、比較の型を合わせる修正です。

person.city === Some("Yokohama")
containsメソッド

ScalacticではScalazのelementメソッドに相当する機能はないようなので===メソッドを使って対応してみます。

エラー検出

containsメソッドの代わりにexistsメソッドを使い、同定比較に===メソッドを使ってみました。

persons.filter(x => cities.exists(_ === x.city))
[error] .../src/main/scala/sample/Main.scala:29: types String and Option[String] do not adhere to the type constraint selected for the === and !== operators; the missing implicit parameter is of type org.scalactic.CanEqual[String,Option[String]]
[error]     persons.filter(x => cities.exists(_ === x.city))
[error]                                         ^

containsメソッドを使うより若干ロジックが入りますが、気になるほどではないと思います。

対応

コンパイルエラーに対する対応は、比較の型を合わせる修正です。

persons.filter(x => cities.exists(y => x.city === Some(y)))

ScalazとScalacticの使い分け

型安全な同定比較機能としてScalazとScalacticで同等の使い勝手の機能が提供されていることが確認できました。

Scalazは汎用のMonadicプログラミング向けライブラリなので、Scalazを使用している場合はありがたく===メソッド、elementメソッドを使用するのがよいと思います。

Scalacticは同定比較機能として以下の機能を提供しています。

  • Torerance (値の誤差範囲を考慮した比較)
  • 同定比較方法のカスタマイズ (大文字小文字の区別の有無など)

また以下のユーティリティ機能も提供しています。

  • Or/Every (Validation結果の格納)
  • 事前条件の文字列補完
  • スナップショット用の文字列補完

こういったユーティリティ機能を使う場合にはScalacticが選択肢になります。

===メソッドだけ欲しい場合は、どちらを選んでもよいですが暗黙定義の影響範囲が少なそうなScalaticの方が若干使い勝手がよさそうです。

まとめ

地味な話題ですが結構開発工数に影響する==メソッドとcontainsメソッドの問題と、その解決策についてご紹介しました。

防御的プログラミングを心がけるなら==メソッドとcontainsメソッドは使わないぐらいの心持ちでもよいと思います。

Scalazは===メソッドだけのために採用するのは心理的障壁が高そうですが、その場合はScalacticを選ぶとよいでしょう。

Scalacticは、事前条件の文字列補完、スナップショット用の文字列補完の機能が地味に便利なので、そういう意味でも使い出があると思います。

諸元

  • Java 1.7.0_75
  • Scala 2.11.7
  • Scalaz 7.2.0
  • Scalactic 3.0.0-M15

2016年1月31日日曜日

[SDN] 例外とNothing

try/catchで例外処理を書く場合、以下のように例外に対する共通処理を羅列する形になることがあります。

すべての例外に対して同じ処理を行う場合には、大本のjava.lang.Exception(場合によってはjava.lang.Throwable)でキャッチすればよいですが、以下のように例外毎に少しずつ処理が異なる場合には羅列せざるを得ません。

def doSometing(): String = {
  try {
    doSomesing()
  } catch {
    case e: FileNotFoundException =>
      log("file not found")
      throw e
    case e: InterruptedIOException =>
      log("intterupted io")
      throw e
    case e: IOException =>
      log("io")
      throw e
    case NonFatal(e) =>
      log("non fatal")
      throw e
  }
}

上記のプログラムでは以下の処理が例外の共通処理になります。

log("file not found")
throw e

「例外毎に異なったメッセージをログに出力して、受け取った例外を再送出する処理」ですが、ログに出力するメッセージの文言が例外毎に変わってきます。

共通処理化

「例外毎に異なったメッセージをログに出力して、受け取った例外を再送出する処理」を関数化して再利用するために以下の関数を作ってみました。

この処理は例題なので単純なものになっていますが、製品開発ではもっと細かくて複雑な処理が必要になってくる可能性があります。また、システム共通のエラー処理を変更する場合には、エラー処理のロジックが一箇所に集まっていた方が取り回しが楽ということもあります。

def handleError(message: String, e: Throwable) {
  log(message)
  throw e
}

この関数は以下のように例外処理に組み込みます。

try {
    doSomesing()
  } catch {
    case e: FileNotFoundException =>
      handleError("file not found", e)
    case e: InterruptedIOException =>
      handleError("intterupted io", e)
    case e: IOException =>
      handleError("io", e)
    case NonFatal(e) =>
      handleError("non fatal", e)
  }

一見これでよいように思いますが、実は以下のようなコンパイルエラーが出てしまいます。

...
[error]  found   : Unit
[error]  required: String
[error]         handleError("file not found", e)
[error]                    ^
...
[error]  found   : Unit
[error]  required: String
[error]         handleError("intterupted io", e)
[error]                    ^
...
[error]  found   : Unit
[error]  required: String
[error]         handleError("io", e)
[error]                    ^
...
[error]  found   : Unit
[error]  required: String
[error]         handleError("non fatal", e)
[error]                    ^
[error] four errors found

その理由は、handleError関数の返却値がUnitであるため、例外処理を行っているdoSomething関数の返却値Stringと型が合わないためです。

この問題の対策が今回のテーマです。

Stringを返す

利用者側の関数の返却値とhandleError関数の返却値が合わない問題を解決する方法として、返却する型を明示したhandleError関数を用意する方法があります。

たとえば以下のようなhandleErrorString関数を用意する方法です。

def handleErrorString(message: String, e: Throwable): String = {
  log(message)
  throw e
}

しかし、この方法を採ると必要な型ごとに1つ関数を用意する必要があります。用途によっては考えられる解決策ですが、汎用的に適用できる方法ではありません。

型パラメータ

1つの解としては以下のように型パラメータを使う方法があります。

def handleError[T](message: String, e: Throwable): T = {
  log(message)
  throw e
}

handleError関数の使用元が必要とする型が自動的に型Tに設定されhandleError関数の返却値となります。このためコンパイルエラーになりません。

Nothing

前述の型パラメータを使う方法でもよいのですが、Scalaにはこのような目的に使用できるNothingという型があります。

Nothingはすべての型のサブクラス、という特殊な位置付けの型です。一種のワイルドカードのような型です。

以下のようにhandleError関数の返却値をNothingとすると、doSomething関数がどのような型を返すことになっても、そのまま利用することができます。

def handleError(message: String, e: Throwable): Nothing = {
  log(message)
  throw e
}

この例のように最後に例外を送出する処理を行う関数は、実際には値は返すことはないので、返却値の型をNothingしても問題はなく、利用範囲は大きく広がります。

今回の問題に対しては前述の型パラメータを使う方法でも対処可能ですが、Nothingを使った方がより意図が明確になると思います。

参考

未実装のメソッドを定義するのに便利な「???」メソッドも返却値にこのNothingを使っています。「???」メソッドの定義は以下のようになっています。

def ??? : Nothing = throw new NotImplementedError

諸元

  • Java 1.7.0_75
  • Scala 2.11.7

2014年6月18日水曜日

Scala Tips/ライブラリ選択(2014年6月バージョン)

どのようなプログラミング言語を使っても、本格的な応用では基本ライブラリだけでニーズが満たせるということはなく、用途に応じて外部ライブラリを併用することになります。

Scalaプログラミングをする上で、ボク的に常に使用するライブラリというのがだいたい固まって来たのでまとめてみました。

scalaz

以下の理由で手放せないライブラリになっています。

  • 「かんたんScalaz」としてご紹介している各種の便利機能
  • 型クラスMonoidの存在
  • 純粋関数型データ構造Tree

また使用頻度は落ちますが、以下の機能も魅力的です。

  • 型クラスTraverse/Foldable
  • 純粋関数型データ構造EphemeralStream
  • Task/Future

最近scalaz-steramを使っていて、どうもTaskが便利らしい、ということが分かってきました。これから使用頻度が高くなるかもしれません。ScalaのFuture/Promiseとの使い分けが悩ましいですね。

scalaz-stream

大規模データに対する操作や、データ操作を並列処理をするための基盤として最近使い始めました。かなりよいです。

scalax.io

Scalaの基本ライブラリはI/O機能が弱いので、本ライブラリは重宝します。以下の2つを使っています。

  • scala-io-core
  • scala-io-file

scala-arm

色々批判もあるようですが、やっぱり便利です。

自作のクラスでもcloseメソッドやdisposeメソッドを定義すれば、自動的にクローズしてくれる機能をヘビーユースしています。

nscala-time

時間周りの処理はやはりJoda-Timeが便利です。そのScalaラッパーということでnscala-timeを使用しています。

dispatch-core

RESTアクセス用に使っています。

難しい記号を多用するのが弱点ですが、色々便利なのは確か。

scalatest

Specs2との二択ですが、ボクはScalaTestの方を使っています。

play-json

Jsonライブラリは、Play上で使っているplay-jsonをPlay外でも使うようになりました。

他にも便利そうなライブラリはありますが、play-jsonが案外高速のようなので無理をして乗り換えるほどではないかな、と今の所は考えています。

anorm & squeryl

DBアクセスはanormとsquerylを併用しています。

anormは、SqlRowが使いやすいのでこれが目当てです。

簡単にちょちょちょっと書きたい所はsquerylが便利。

ただ、この分野はSlickが使いやすければ乗り換えるかもしれません。

2014年6月13日金曜日

実用Scalaz/Option + Monoid

OptionはScalaプログラミングのキーパーツであり、Optionの捌き方の巧拙がScalaプログラミングの効率に直結します。

これと同様にMonoidはScalazプログラミングのキーパーツということができるかと思います。Monoidの捌き方の巧拙がScalazプログラミング、さらにはMonadicプログラミングの効率に直結することになるでしょう。

そして、ScalazのおいてOptionは代表的なMonoidです。つまり、OptionをMonoidとして捌いていく技法はScalazプログラミング、Monadicプログラミングにおいて最重要のテクニックといえるわけです。

というととても難しい技術のように見えますが、(背景の数学的な理論は別として)使い方はとても簡単で、さらに頻出のユースケースをカバーする非常に便利なイディオムです。

準備

説明の準備に以下の関数を定義します。

scala> def a: Option[Int] = Some(100)
def a: Option[Int] = Some(100)
a: Option[Int]

scala> def b: Option[Int] = None
def b: Option[Int] = None
b: Option[Int]

よくある処理

プログラムを書いているとよく出てくるのが以下のような処理です。

def plus(lhs: Option[Int], rhs: Option[Int]): Option[Int] = {
  lhsの中身とrhsの中身を加算する
  ただしlhsまたはrhsのどちらか一方がNoneの場合はSomeの方の値を使用する
  両方がNoneの場合はNoneにする
}

よくある間違い

この処理を書く場合、Optionを使ってMonadicに処理すると行けそうに思えます。そこで、次のような処理を書いたとしましょう。

def plus(lhs: Option[Int], rhs: Option[Int]): Option[Int] = {
d lhs.flatMap(x => rhs.map(y => x + y))
}

これは、for式を使って以下のように書くこともできます。

def plus(lhs: Option[Int], rhs: Option[Int]): Option[Int] = {
  for (x <- lhs; y <- rhs) yield x + y
}

さて、この結果ですが、以下のようになります。

scala> plus(a, a)
res1: Option[Int] = Some(200)

scala> plus(a, b)
res3: Option[Int] = None

scala> plus(b, a)
res4: Option[Int] = None

scala> plus(b, b)
res2: Option[Int] = None

Monadicに処理した場合は、残念ながらいずれかのOptionがNoneだった場合に、結果もNoneになってしまうわけです。このような処理が適切なケースも多いわけですが、前述の「plus関数」の定義とは異なるのでこの実装は使えません。

Match式

「plus関数」をmatch式を使って実装すると以下のようになります。

def plus(lhs: Option[Int], rhs: Option[Int]): Option[Int] = {
  (lhs, rhs) match {
    case (Some(l), Some(r)) => Some(l + r)
    case (Some(l), None) => Some(l)
    case (None, Some(r)) => Some(r)
    case (None, None) => None
  }
}

このようなOptionの組み合わせごとに処理を分けるmatch式はScalaプログラミングをしているとよく出てくるのではないでしょうか?しかも、かなり野暮ったい感じなのでこれを簡単に書けると好都合です。

Monoid

前出のmatch式による処理をScalazのMonoidで書くと以下になります。複雑なmatch式を演算子「|+|」のみで記述できています。

ScalazではOptionのMonoid演算として、前述のmatch式相当の演算を割り当てているわけですね。

def plus(lhs: Option[Int], rhs: Option[Int]): Option[Int] = {
  lhs |+| rhs
}

この「plus」関数の実行結果は以下になります。

scala> plus(a, a)
res9: Option[Int] = Some(200)

scala> plus(a, b)
res10: Option[Int] = Some(100)

scala> plus(b, a)
res11: Option[Int] = Some(100)

scala> plus(b, b)
res13: Option[Int] = None

おさらい

おさらいの意味でOptionに対してMonoidの演算子「|+|」を適用して演算を行った結果を以下に示します。

scala> 1.some |+| 2.some
1.some |+| 2.some
res40: Option[Int] = Some(3)

scala> 1.some |+| none[Int]
1.some |+| none[Int]
res54: Option[Int] = Some(1)

scala> none[Int] |+| 2.some
none[Int] |+| 2.some
res42: Option[Int] = Some(2)

scala> none[Int] |+| none[Int]
none[Int] |+| none[Int]
res53: Option[Int] = None

参考

Option Index

Optionに関する2012年ごろのブログのまとめです。

Monoid Index

Monoidに関する2012年ごろのブログのまとめです。

諸元

  • Scala 2.10.4
  • Scalaz 7.0.6

2014年6月2日月曜日

かんたんScalaz/Option編 SomeとNone

Optionを使う場合にSomeやNoneを作成する処理は当然ながら頻出です。

scala> val a = Some(5)
val a = Some(5)
a: Some[Int] = Some(5)

scala> val b = None
val b = None
b: None.type = None

通常はこれで問題ないのですが、若干扱いにくい点があります。

上記処理で得られる型にご注目下さい。

「Some(5)」を代入(束縛?)した変数aの型がSome[Int]になっています。さらに、Noneを代入した変数の方がNone.typeになっています。

「Some(5)」や「None」は、型「Option[Int]」の値として扱われる事が多いので、「Some(5)」や「None」を型「Option[Int]」として生成する手段があるとさらに便利になります。

たとえば以下のようなメソッド定義のケースです。この場合、メソッドの返却値の型の定義を省略することができます。

scala> def getFoo = Some(5)
def getFoo = Some(5)
getFoo: Some[Int]

scala> def getBar = None
def getBar = None
getBar: None.type

またよくあるのは以下のような畳み込み処理の初期値です。畳み込みの初期値の型が「Option[Int]」ではなく「Some[Int]」になっているため、式全体の演算結果としてNoneを返すことができなくなっています。

scala> Vector(1, 2, 3, 4, 5).foldLeft(Some(0))((z, x) => if (x % 2 == 0) None else z.map(_ + x))
se z.map(_ + x))
<console>:14: error: type mismatch;
 found   : None.type
 required: Some[Int]
              Vector(1, 2, 3, 4, 5).foldLeft(Some(0))((z, x) => if (x % 2 == 0) None else z.map(_ + x))
                                                                                ^

Scalaの解

まず、この問題に対するScalaでの対策です。

値がある場合はOption#applyメソッドを使用、値がない場合はOption#emptyメソッドに型「Int」を指定することで型「Option[Int]」を得ることができます。

scala> Option(5)
Option(5)
res14: Option[Int] = Some(5)

scala> Option.empty[Int]
Option.empty[Int]
res16: Option[Int] = None

前述の例をこの方法で書きなおしたものが以下になります。

scala> def getFoo = Option(5)
def getFoo = Option(5)
getFoo: Option[Int]

scala> def getBar = Option.empty[Int]
def getBar = Option.empty[Int]
getBar: Option[Int]

scala> Vector(1, 2, 3, 4, 5).foldLeft(Option(0))((z, x) => if (x % 2 == 0) None else z.map(_ + x))
else z.map(_ + x))
res17: Option[Int] = None

Scalazの解

Scalazではこの問題に対応するため、任意の値を型「Option[T]」のSome[T]またはNoneに持ち上げる機能を提供しています。

scala> 5.some
res30: Option[Int] = Some(5)

scala> none[Int]
res33: Option[Int] = None

前述の例をこの方法で書き直したものが以下になります。

scala> def getFoo = 5.some
getFoo: Option[Int]

scala> def getBar = none[Int]
getBar: Option[Int]

scala> Vector(1, 2, 3, 4, 5).foldLeft(0.some)((z, x) => if (x % 2 == 0) None else z.map(_ + x))
e z.map(_ + x))
res18: Option[Int] = None

個人的な好みの問題もありますがScala版に対して以下の優位点があると思います。

  • プログラミング時に書きやすい
  • 可読性が高い

具体的には「5.some」の記法は「Option(5)」の記法と比べて以下の優位点があると思います。

プログラミング時の書きやすさ

Scala版では、プログラミング時に「5」と書いた後にSome化したいと思った場合、一度カーソルを前に戻して「Some(」を書いた後、カーソルを文末に移動させて「)」を閉じるという作業が必要になります。それに対して、Scalaz版では「5」と書いた後にSome化したいと思った場合「.some」と書くだけですみます。プログラミングの流れを止めません。

またScala版では「Some(5)」が欲しい時に、場合によっては「Option(5)」にしないといけないので、毎回判断が必要になります。Scalaz版ではどの場合でも「5.some」でOKです。

可読性

Scala版では「Some(5)」の意味で「Option(5)」を使うことがありますが、Optionというオブジェクト名を使用するため、微差ですが意図がわかりづらい面があると思います。

別の切り口として、重要な情報が左側に来る方が可読性が高くなると思います。これを前提にすると、Scala版の「Some(5)」の場合「Some」であることが重要で、「5」の重要度はその次という印象になります。一方、Scalaz版の「5.some」の場合「5」の値が重要で、「some」の重要度はその次という印象になります。多くの場合、「5」の方が重要なので、(これまた微差ですが)「Some(5)」より「5.some」の方が可読性が優れているのではないかと思います。

Scalaz版のまとめ

「プログラミング時の書きやすさ」、「可読性」のいずれも微差なので、好みの方法を使えばよいわけですが、ボクはScalazの提供する「5.some」、「none[Int]」の記法を愛用しています。

参考

今回の記事は、基本的には2012年の以下の記事と同じ内容を再整理したものです。

棚卸しという意味で記事化しました。

諸元

  • Scala 2.10.4
  • Scalaz 7.0.6

2014年5月26日月曜日

かんたんScalaz/Option編

かんたんScalazのOption編です。

Scalaプログラミングでは、Optionはキーとなる重要なオブジェクトで、Optionの取り回し方の優劣がプログラミングの精度・効率に大きく作用します。

ScalazでもOption向けに多くの機能を用意しています。

「かんたんScalaz」ということでScalazが提供しているMonadやMonoidといった難しい概念を抜きにして便利に使えるOptionの機能をまとめてみました。

準備

説明の準備に以下の関数を定義します。

scala> def a: Option[Int] = Some(100)
def a: Option[Int] = Some(100)
a: Option[Int]

scala> def b: Option[Int] = None
def b: Option[Int] = None
b: Option[Int]

?と|

OptionをBoolean型に見立てて値を決めたい場合に、3項演算子的な書き方ができると便利です。Scalazでは「?」と「|」の組合せでこれが実現できます。

scala> a ? "defined" | "undefined"
a ? "defined" | "undefined"
res12: String = defined

scala> b ? "defined" | "undefined"
b ? "defined" | "undefined"
res13: String = undefined

Scala基本機能のみでもif式で以下のように書ける上に実行性能も圧倒的にこちらの方が速いので、通常はこれでも十分ですが、上記の方が短く書けるので好みによって使ってみるのもよいと思います。

scala> if (a.isDefined) "defined" else "undefined"
if (a.isDefined) "defined" else "undefined"
res44: String = defined

個人的には「?」と「|」の方が可読性が高いように思うので、性能が気にならないところ(例:フレームワークのコア機能以外)ではよく使っています。

|

OptionがSomeだった場合は格納された値、そうでなかった場合はデフォルト値を取得する処理はよく出てきます。

通常は、OptionのgetOrElseメソッドを使いますが、Scalazでは「|」でこれを記述することができます。

scala> a | 10
a | 10
res17: Int = 100

scala> b | 10
b | 10
res18: Int = 10

個人的には「|」の方が可読性が高いように思うので、性能が気にならないところ(例:フレームワークのコア機能以外)ではよく使っています。

some/none

OptionがSomeだった場合は格納された値に演算を施した値、そうでなかった場合はデフォルト値を取得する処理もよく出てきます。

Scalaの基本機能で記述する場合、以下のようになると思います。

scala> a match {
  case Some(x) => x + 100
  case None => 0
}
res45: Int = 200

scala> a.map(_ + 100).getOrElse(0)
a.map(_ + 100).getOrElse(0)
res46: Int = 200

どちらの方法でもよいですが、頻出処理なのでもっと簡略化した記法が使えるとうれしいところです。

Scalazではこの目的で「some」と「none」の組合せでの記述方法を用意しています。

scala> a some(_ + 100) none(0)
a some(_ + 100) none(0)
res15: Int = 200

scala> b some(_ + 100) none(0)
b some(_ + 100) none(0)
res16: Int = 0

また前述の「|」を使用した以下の記述方法も便利です。

scala> a.map(_ + 100) | 0
a.map(_ + 100) | 0
res47: Int = 200

orZero

orZeroメソッドは「かんたんScalaz/Boolean編」で紹介した「??」メソッドや「!?」メソッドで用いているMonoidの性質を利用したメソッドです。

OptionがSomeの場合は格納された値を返しますが、そうでない場合は格納された値のMonoidとしての単位元を返します。このためMonoidである型にしか適用できませんが、基本データ型やコレクションなどはほとんどMonoidなのでかなり適用範囲は広いです。

scala> a.orZero
a.orZero
res22: Int = 100

scala> b.orZero
b.orZero
res23: Int = 0

上記プログラムの動きは以下のようになります。

OptionがSomeの場合は、Someに格納されている値である100が返ります。一方Noneの場合は、以下の動きになっています。

  • Optionに格納されている型はInt
  • Intの単位元は「0」
  • OptionがNoneなのでIntの単位元である「0」を返す

Monoidというと敷居が高いので、「かんたんScalaz」の文脈では型ごとに初期値を持っている、ぐらいの捉え方でよいと思います。この「初期値」は、数値系なら「0」、ListなどのコレクションはNilなどの「空」となりますので、0や空といった値になると覚えておいて、実際の値は必要に応じて調べるというアプローチでよいでしょう。

「〜」を使う以下の書き方も用意されていますが、見落としや誤読してしまいそうなのでボクは使わないようにしています。

scala> ~a
res48: Int = 100

scala> ~b
res49: Int = 0

参考

Option Index

Optionに関する2012年ごろのブログのまとめです。

Optionに関しては、この当時とそれほど見方は変わっていません。このため、このあたりの記事も現役として参考にしていただけると思います。

ただ、プログラミングしている中で色々なバランスが見えてきた部分もあると思うので、棚卸しをした上で適宜追加情報や新しいバランス上での使い方についてまとめていきたいと思います。

諸元

  • Scala 2.10.4
  • Scalaz 7.0.6

2014年5月19日月曜日

かんたんScalaz/Booean論理記号

ScalazではBooleanに以下の論理演算子を追加しています。







演算子意味別名Scala
∧Conjunction, AND/\&&
∨Disjunction, OR\/||
!||Negation of Disjunction, NOR
!&&Negation of Conjunction, NAND
ーー>Conditional
<ーーInverse Conditional
⇏Negational of Conditionalー/>
⇍Negation of Inverse Conditional<\ー

かんたんScalazの観点では、以下のような扱いがよいと思います。

  • 「ならば」の論理演算である「ーー>」は覚える価値がある。
  • 他の演算子は必要に応じて。

ーー>

Scala(や通常のプログラミング言語)が提供していない論理演算子の中で「ならば(Conditional, 論理包含、条件文)」を表す論理演算子「ーー>」はなかなか有用ではないかと思います。「ーー>」の使い所はたとえば以下のようなassertです。

assert (!isCacheable(x) --> !isCached(x), "キャッシュ可能でない場合、キャッシュにデータが入っているのはおかしい。")

このケースだと、キャッシュ可能の場合はキャッシュにデータがあってもなくてもよいので、「ならば」の論理演算の条件にぴったり合います。

ちなみに、上記のassertは「<ーー」(Inverse Conditional, 逆論理包含?)を使って以下のようにも書けるようです。

assert (isCacheable(x) <-- isCached(x), "キャッシュにデータが入っている場合、キャッシュ可能である必要がある。")

∧と∨

論理積の演算子に「∧」や「/\」、論理和の演算子に「∨」や「\/」が定義されていますが、Scalaの論理演算子「&&」、「||」とかぶるので使用しても通常のプログラミングが特に便利になるというものではありません。逆に実行時のオーバーヘッドは確実にあるので、「∧」や「∨」といった数学記号をどうしても使いたい局面でなければ無理をして使う感じでもないでしょう。

否定形

以下のものは他の演算子の否定形です。否定形があると覚えておいて、使う時に仕様を確認する形でよいでしょう。





論理記号別名否定元
!||||
!&&&&
⇏ー/>ーー>
⇍<\ー<ーー

諸元

  • Scala 2.10.4
  • Scalaz 7.0.6

2014年5月12日月曜日

かんたんScalaz/Boolean編

Scalaz 7を本格的に使い始めたので、Scalaz 7を包含したコーディング・イディオムの棚卸しをしています。Scala 2.10の基本機能とScalaz 7を併用する前提で、コーディングの局面毎に使用するイディオムを事前に準備しておくわけです。イディオムを事前準備しておくことで、プログラミングの局面局面で即断即決ができるので効率よくプログラミングを進めることができます。またイディオムは利便性や実行速度などの要因を考慮に入れて使い所を絞り込んでおくので、プログラムの品質や性能などの向上も期待できます。

Scalazは、MonadやMonoidといった代数系の概念を軸にした型クラスやや純粋関数型データ構造を提供するライブラリですが、小回りの効いた便利機能も数多く提供しています。これらの便利機能は特にMonadやMonoidといった概念を抜きにして、便利につかえるので「かんたんScalaz」としてまとめていきたいと思います。

今回はBoolean向けの便利機能です。

準備

説明の準備に以下の関数を定義します。

scala> def t: Boolean = true
def t: Boolean = true
t: Boolean

scala> def f: Boolean = false
def f: Boolean = false
f: Boolean

option

ScalazではBooleanにoptionメソッドを追加しています。optionメソッドは、Booleanがtrueであれば指定された値を返すSomeを、falseであればNoneを返します。

使い方は以下になります。

scala> t option 100
t option 100
res3: Option[Int] = Some(100)

scala> f option 100
f option 100
res4: Option[Int] = None

これをScalaネイティブの文法で書くとif式を用いた以下になります。これでも問題ないといえばないのと実効速度は確実に速いので、Scalazのoptionメソッドは無理をして使う必要はありませんが、プログラミング時にはかゆいところに手が届くという感じで便利なんですよね。

if (t) Option(100) else None

たとえば、次のような感じでBooleanがtrueの時の処理を長めに書く時に重宝します。

t option {
  val a = somework
  val b = somework(a)
  somework(b)
}

??と!?

ScalazではMonoidという型クラスを提供していますが、これがすこぶる便利なんです。

その便利さの一つが単位元です。Monoidの性質を持つオブジェクトは、単位元という特別なインスタンスが定義されます。この単位元をデフォルト値や初期値として用いることで、プログラミングを簡略化するテクニックがありますが、この実例の一つがBooleanの「??」メソッドと「!?」メソッドです。

「??」メソッドは、Booleanがtrueであれば指定された値を、falseの場合は型の単位元を返します。

具体的には以下のような動きになります。

scala> t ?? 100
t ?? 100
res5: Int = 100

scala> f ?? 100
f ?? 100
res6: Int = 0

Booleanの値がtrueの場合は、指定された値である100が返ります。一方falseの場合は、以下の動きになっています。

  • 指定された値「100」の型はInt
  • この式全体の型はInt
  • Intの単位元は「0」
  • Booleanの値がfalseなのでIntの単位元である「0」を返す

このようにMonoidの性質とScalaの型推論のコンビネーションで、非常にコンパクトに目的の式を記述することができるわけです。

Monoidというと敷居が高いので、「かんたんScalaz」の文脈では型ごとに初期値を持っている、ぐらいの捉え方でよいと思います。この「初期値」は、数値系なら「0」、ListなどはNilとなりますので、0や空といった値になると覚えておいて、実際の値は必要に応じて調べるというアプローチでよいでしょう。

?!

「??」メソッドの否定形は「?!」メソッドになります。

scala> t !? 100
t !? 100
res165: Int = 0

scala> f !? 100
f !? 100
res167: Int = 100

? |

Javaで提供されていた3項演算子は、Scalaには取り込まれずif式を使用するという建付けになっています。

scala> if (t) 100 else 200
if (t) 100 else 200
res0: Int = 100

Scalazでは「?」メソッドと「|」メソッドのコンビネーションで3項演算子っぽく記述することが可能です。

scala> t ? 100 | 200
t ? 100 | 200
res1: Int = 100

scala> f ? 100 | 200
f ? 100 | 200
res2: Int = 200

「?」メソッド&「|」メソッドとは別にfoldメソッドというのも用意されていますが、こちらは特段便利になったような感じはしません。if式と比べると実行速度は確実に落ちるので、foldを使うのならif式を使う方がよさそうです。

scala> t fold (100, 200)
t fold (100, 200)
res9: Int = 100

scala> f fold (100, 200)
f fold (100, 200)
res10: Int = 200

「?」メソッド&「|」メソッドもif式に比べると実行速度は確実に落ちるので無理をして使うほどのことはありませんが、短くかけるとプログラミングが楽になる局面(式を1行に収めたい等)はあるので、コーディング・イディオムに加えておくのもよいと思います。

参考

Boolean

2012年のブログです。

Booleanについてはあまり認識は変わっていないようです。

ただ、2012年当時とは色々とアプローチが変わった部分もあるので、技術の棚卸しという意味で過去のブログで取り上げた内容も再度取り上げていく予定です。

Monoid

Monoidに関する2012年ごろのブログのまとめです。

Scalazが提供する代数概念の型クラスのうち、MonadはScalaネイティブでも基本機能は持っていますし、Aplicativeはfor式で代替することが可能です。

しかし、Monoidだけは該当する機能がありません。このMonoidが非常に便利なので、これを使うためにScalazを使うという面が大きいです。

諸元

  • Scala 2.10.4
  • Scalaz 7.0.6

2012年12月19日水曜日

Scala Tips / トレイトでクラス分割

Scalaプログラミングの便利さを日々体感しているわけですが、その重要な要素の一つがトレイト。トレイトは関数型言語のものではなく、オブジェクト指向プログラミングの技術ですが、そのあるなしはプログラミング・モデルに大きく影響します。

Scalaにおけるクラス設計は、このトレイトの存在だけを意識するとしてもJavaのそれとは別ものということができます。

トレイトには色々な用途がありますが、最近ボクが地味に多用しているのがクラスの分割です。機能数が増えて大きくなったクラスを幾つかのトレイトに分割するだけなのですが、プログラムの見通しがよくなり、また機能間の依存関係も明確になるので、大変重宝しています。

クラス分割の例

以下のクラスがあるとします。変数aに設定した値をメソッドbとメソッドcが使用しています。

class Whole {
  val a = 1
  def b = a + 1
  def c = a + 2
}

このクラスを以下の3つの部品(クラス+トレイト)に分解し、これらの部品を組み立ててクラスを定義します。

Whole
クラス本体。変数aを定義している。
Parta
部品となるトレイト。変数aを使用するメソッドbを定義している。
Partb
部品となるトレイト。変数aを使用するメソッドcを定義している。
class Whole extends Parta with Partb {
  val a = 1
}

trait Parta {
  self: Whole =>

  def b = a + 1
}

trait Partb {
  self: Whole =>

  def c = a + 2
}

ポイントとなるのがPartaのメソッドbとPartbのメソッドcがクラスWholeの変数aに依存していること。普通はこういう場合、クラスWholeとトレイトb, cはインヘリタンスの関係が必要になってきます。

しかし、この場合はトレイトPartaとPartb内のself-type annotationである「self: Whole =>」によって、トレイトがWholeにmixinされることが保証されるためクラスWholeの変数aを使用することができます。

そしてクラスWholeの定義で「class Whole extends Parta with Partb」とすることで、クラスWholeにトレイトPartaとPartbの機能を追加することができます。

この場合、トレイトPartaとトレイトPartbの間には依存関係がないことが明確に分かります。このような形で、機能間の依存関係をできるだけ疎にしていくのが、大きなプログラムを作るときのコツとなります。

実行

実行結果は以下のとおりです。普通に動作しました。

scala> val w = new Whole
w: Whole = Whole@2bf75442

scala> w.a
res0: Int = 1

scala> w.b
res1: Int = 2

scala> w.c
res2: Int = 3

諸元

  • Scala 2.9.2

2012年12月14日金曜日

Scala Tips / Streamで採番(2)

Streamで採番では、Streamのfromメソッドを使って数値の連番による番号の採番を行いました。

場合によっては、採番する番号として「A00001」というような英数字と数字の組合せを取りたい場合があります。

このような場合は、カスタムでStreamを作ることになります。

といっても、これは簡単で以下のような再帰関数のイディオムを使います。

def nums(i: Int): Stream[String] = {
  "A%05d".format(i) #:: nums(i + 1)
}

使ってみる

それでは新しく作ったnums関数による採番用Streamを使ってみましょう。

採番した値の型はStringになるのNumberedNameを修正します。

case class NumberedName(name: String, number: String)

シーケンスに対して採番を行い値と採番した番号のTupleを返す関数は以下になります。Stream#fromの代わりにnums関数を使っています。

def f(names: Seq[String]): Seq[NumberedName] = {
  for ((n, c) <- names zip nums(1)) yield {
    NumberedName(n, c)
  }
}

実行

それでは実行してみましょう。

まずテスト用のListを定義します。

scala> val xs = List("a", "b", "c")
xs: List[java.lang.String] = List(a, b, c)

このListに対する関数fの実行結果は以下になります。

scala> f(xs)
res6: Seq[NumberedName] = List(NumberedName(a,A00001), NumberedName(b,A00002), NumberedName(c,A00003))

ごくごく簡単に使うことができますね。

ノート

再帰関数とStreamを使うことで、採番ロジックを関数として部品化することができました。この部品はSeqのzip関数でシーケンスと結びつけることができます。

こういった疎結合の関数部品を用意して、関数合成のテクニックで組合せて使うのが、関数型らしいプログラミングです。Streamの活用はそのような関数型プログラミングを行う上で重要なテクニックになっています。

諸元

  • Scala 2.9.2

2012年11月19日月曜日

Scala Tips / Seq, Function1, PartialFunction, Option

MapがFunction1かつPartialFunctionというのが案外盲点になりますが、SeqすなわちList, Stream, VectorもFunction1かつPartialFunctionというのも見過ごしがちです。

以下の関数fを考えます。「Intを引数に取りStringを返す関数」を第一引数に、Int値を第二引数に取り、Int値に対応するStringを返します。

def f(a: Int => String)(b: Int): String = {
  a(b)
}

準備

Seqの例としてListを定義します。

scala> val l = List("zero", "one", "two", "three")
l: List[java.lang.String] = List(zero, one, two, three)

ついでにMapも定義しておきます。

cala> val m = Map(4 -> "four", 5 -> "five")
m: scala.collection.immutable.Map[Int,java.lang.String] = Map(4 -> four, 5 -> five)

関数fの第一引数にListを指定するとList(Seq)はFunction1のためそのまま普通に動作します。

ListのインデックスがFunction1の引数に対応しており、引数に指定された数値に対応するListの内容を返します。

scala> f(l)(3)
res6: String = three

「Map, Function1, PartialFunction, Option」で説明したようにMapもFunction1なので普通に動作します。

scala> f(m)(5)
res7: String = five

Listの範囲外の数値を指定すると例外が発生します。

scala> f(l)(10)
java.lang.IndexOutOfBoundsException: 10
 at scala.collection.LinearSeqOptimized$class.apply(LinearSeqOptimized.scala:51)
 at scala.collection.immutable.List.apply(List.scala:76)
 at scala.collection.immutable.List.apply(List.scala:76)
 at .f(<console>:12)
 at .<init>(<console>:14)
 at .<clinit>(<console>)
 at .<init>(<console>:11)
 at .<clinit>(<console>)
 at $print(<console>)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
 at java.lang.reflect.Method.invoke(Method.java:597)
 at scala.tools.nsc.interpreter.IMain$ReadEvalPrint.call(IMain.scala:704)
 at scala.tools.nsc.interpreter.IMain$Request$$anonfun$14.apply(IMain.scala:920)
 at scala.tools.nsc.interpreter.Line$$anonfun$1.apply$mcV$sp(Line.scala:43)
 at scala.tools.nsc.io.package$$anon$2.run(package.scala:25)
 at java.lang.Thread.run(Thread.java:680)

Option化

Listの範囲外の数値を指定された時にもきちんと動作させるためにはFunction1ではなくPartialFunctionを使用します。PartialFunctionはliftメソッドでOption化されたFunction1に変換されるので、これを利用します。

Option化した関数fは以下になります。

def f(a: PartialFunction[Int, String])(b: Int): Option[String] = {
  a.lift(b)
}

List(Seq)もMapもPartialFunctionなので、そのまま関数fの引数に指定することができます。

動作結果は以下のとおりです。

scala> f(l)(3)
res9: Option[String] = Some(three)

scala> f(m)(5)
res10: Option[String] = Some(five)

scala> f(l)(10)
res11: Option[String] = None

PartialFunctionの合成

PartialFunctionはorElseを使って合成するのが、よく出てくるテクニックです。

関数fの引数として、lとmをorElseで合成したものを指定すると以下のように動作します。

scala> f(l orElse m)(3)
res12: Option[String] = Some(three)

scala> f(l orElse m)(5)
res13: Option[String] = Some(five)

scala> f(l orElse m)(10)
res14: Option[String] = None

Int値が0から3の場合は変数lのListに、4と5の場合は変数mのMapが対応する値を返します。それ以外の数値の場合は対応できるPartialFunctionがないためNoneが返ります。

ノート

「Map, Function1, PartialFunction, Option」のMapも、今回のListも、Function1かつPartialFunctionであるというのが関数型プログラミング的には重要なポイントです。

関数型プログラミングでは、ファンクタの対象となるA→Bの関数、モナドの対象となるA→M[B]の関数(Mはモナド)が非常に重要な意味を持ちます。この形をした関数を部品として合成していくのが関数型プログラミングの基本的な考え方になるためです。

MapやSeqはFunction1つまりA→Bの関数です。またPartialFunctionであることはliftメソッドでA→Option[B]というA→M[B]の形に持ち込めます。つまり、MapやSeq特有の機能は持ちながら、いざとなればA→BやA→M[B]の関数としても使える点がプログラミングでの選択肢を広げることにつながっているわけです。

諸元

  • Scala 2.9.2

2012年11月16日金曜日

Scala Tips / Emacs

ScalaプログラミングはなんといってもEmacs+Ensimeですね!

先週サブノート目的でMacBook Air 11inchを入手したのですが、Emacs+Ensimeで満ち足りてしまって、まだEclipseは入れていない状況です。

Emacsのどこがよいかというと、まずテキストエディタとしての基本機能、シェルモードやディレクトリモードといったシェル機能が充実している点が挙げられますが、なんといってもelispで比較的簡単にカスタマイズができるのがよいですね。

カッコの捌き方

Scalaプログラミングをする時の生産性に影響があるのが「{」「}」の捌き方です。たとえば:

abc {

といれると、自動的に括弧を開いてくれてカーソルを所定の位置に持って行ってくれると便利です。

abc {
  ←カーソル
}

もちろん、これぐらいの自動化はどのエディタにもついていると思いますが、問題なのは「abc { }」と打った時に自動的・強制的に上の形になってしまうと、逆に「abc { x => x + 1 }」と書きたかった時に手戻りが発生してしまいます。

このような問題があるため、キーの入力で自動的に上記のような整形をする機能(Emacsのscala-modeではscala-mode-feature-electric)は案外使いづらく、ボクもたいてい切ってしまいます。

abc {
  ←カーソル
}

にしたい時と

abc { x => x + 1 }

にしたい時のどちらにも対応できる入力方法が欲しいところです。

カスタマイズ

この問題に対処するために、以下のelispの関数を作ってみました。

(defun my-scala-newline(arg)
  (interactive "p")
  (cond ((scala-in-multi-line-comment-p)
  (scala-newline))
 ((char-equal ?\} (following-char))
  (let (killed)
           (newline-and-indent)
           (newline-and-indent)
           (forward-char)
    (setq killed (not (my-end-of-line-p)))
    (if killed (kill-line))
           (previous-line)
           (indent-for-tab-command)
    (if killed (yank))))
 (t
  (newline-and-indent))))

.emacsまたはinit.elへの設定は以下になります。

(require 'scala-mode-feature-electric)

(setq scala-mode-feature:electric-expand-delimiters-list '(?\{))

(add-hook 'scala-mode-hook
          (function (lambda ()
        (scala-mode-feature-electric-mode)
          (define-key scala-mode-map "\r" 'my-scala-newline)

まずscala-mode-feature:electric-expand-delimiters-listで「{」のみを有効にするように設定しています。こうすることによって:

abc {

と打つと自動的に以下のようになります。

abc { }←カーソル

ここからの動きがポイントですが、先程のmy-scala-newline関数がReturnに設定されていると、この場所でReturnを押すことで、以下の形に整形されカーソルも所定の位置に移動します。

abc {
  ←カーソル
}

カーソルの位置が他の場所の場合、通常の改行キーの動作をするので、不必要な整形が行われません。「abc { x => x + 1 }」と書きたい時はそのまま入力をしていけばよいわけですね。

括弧を追加する場合の捌き方

my-scala-newline関数は、よく出てくるコーディングパターンをサポートするためにもう一工夫しています。

以下のように一行に書いていた文を:

abc xyz

括弧を入れて複数行に分割したい時がよくあります。

abc {
  xyz
}

まず、最初の状態で「abc 」の場所で「{」を打つと以下のようになります。カーソルは「}」の上になります。

abc { }xyz

ここでReturnを押下すると、「}」の後ろのxyzが自動的に括弧内に移動し、カーソルも所定の位置に移動します。

abc {
  xyz←カーソル
}

簡単なカスタマイズですが、なかなか効果抜群です。

2012年11月15日木曜日

ScalaTips / Map, Function1, PartialFunction, Option

クライアントから受け取ったリクエストや定義ファイルの情報などをMapに格納して、プログラム内で持ちまわることはよくあります。

このような場合、以下のように関数の引数にMapを渡します。

def f(config: Map[String, String]) {
  println(config("name"))
  println(config("url"))
}

以下のようなMapがある場合:

scala> val map = Map("name" -> "Taro", "url" -> "http://www.example.com")
map: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(name -> Taro, url -> http://www.example.com)

関数fの動作は以下のようになります。

scala> f(map)
Taro
http://www.example.com

MapはFunction1

JavaだとMapを受け取るメソッドを書いたところで終了ですが、Scalaの場合MapはFunction1という特徴があるので、もう少し応用範囲が広がります。

以下の関数fでは引数がMapではなくFunction1になっています。

def f(config: String => String) {
  println(config("name"))
  println(config("url"))
}

ScalaのMapはFunction1でもあるので、以下のようにこの関数にMapを指定することができます。

scala> f(map)
Taro
http://www.example.com

関数fの設計を考える場合、Function1がMapを包含しているわけですから、Function1を引数に取ったほうが応用範囲が広がり、より望ましい選択と言えます。

値がない場合を考慮

関数の引数にMapを取る場合、Mapの要素として指定した値がないことを考慮したい場合があります。この時はOptionを返すgetメソッドを用いて処理を切り分けます。

def f(config: Map[String, String]) {
  config.get("name").foreach(println)
  config.get("url").foreach(println)
}

値がある場合は以下のようになります。

scala> f(map)
Taro
http://www.example.com

値がない場合もきちんと動作します。

scala> f(Map.empty)

PartialFunction

値がない場合を扱うことができる関数としてScalaではPartialFunctionを提供しています。PartialFunctionのisDefineAtメソッドとapplyメソッドを駆使すると以下のように値がない場合の切り分け処理を記述することができます。

def f(config: PartialFunction[String, String]) {
  if (config.isDefinedAt("name")) {
    println(config("name"))
  }
  if (config.isDefinedAt("url")) {
    println(config("url"))
  }
}

ScalaのMapはFunction1であると同時にPartialFunctionでもあります。このため、この関数fにもそのまま指定することができます。

scala> f(map)
Taro
http://www.example.com

値がないときも無事動作しました。

scala> f(Map.empty)

PartialFunctionをFunction+Optionにlift

先ほどの関数fは、PartialFunctionの扱いが手続き型チックだったので、もう少し関数型っぽくしてみます。

具体的にはliftメソッドを用いて、PartialFunction[String, String]をFunction1[String, Option[String]]に持ち上げます。

def f(config: PartialFunction[String, String]) {
  val c: String => Option[String] = config.lift
  c("name").foreach(println)
  c("url").foreach(println)
}

実装は変わりましたが、インタフェースは変わらないのでMapを指定しても同じように動作します。

scala> f(map)
Taro
http://www.example.com

scala> f(Map.empty)

Option

今度は逆に、関数fの引数がFunction1[String, Option[String]]だった場合です。

def f(config: String => Option[String]) {
  config("name").foreach(println)
  config("url").foreach(println)
}

この場合は、Map[String, String]を指定するとエラーになってしまいます。

scala> f(map)
<console>:10: error: type mismatch;
 found   : scala.collection.immutable.Map[java.lang.String,java.lang.String]
 required: String => Option[String]
              f(map)
                ^

ここで登場するのがPartialFunctionのliftメソッドです。MapもPartialFunctionなので、このliftメソッドを使ってFunction1[String, Option[String]]に持ち上げることができます。

以下のように無事関数fに適用できました。

scala> f(map.lift)
Taro
http://www.example.com

ノート

日々のScalaプログラミングでよく出てくるMap, Function1, PartialFunction, Optionの連携を簡単にまとめてみました。

MapがFunction1かつPartialFunctionであるということは案外盲点で、このことを知っておけば関数のシグネチャでMapより応用範囲の広いFunction1やPartialFunctionを選択できるようになります。

また、Mapが出てくるような局面では値がない場合があることが普通なので、Function1よりもPartialFunctionがより望ましい選択になります。PartialFunctionが出てくるとOptionの活用も視野に入ってきます。

関数の引数にPartialFunctionを使うのか、Function1+Optionを使うのかはケースバイケースですが、Mapのliftメソッドの存在を知っておけば、どちらがきた場合でも対処できます。

諸元

  • Scala 2.9.2

2012年11月8日木曜日

Scala Tips / Streamで脱出

関数型プログラミングでは、遅延評価を用いて大量データを処理するのが定番のテクニックになっています。パズルを解く問題のアルゴリズムによく出てきますね。

ただ、業務アプリケーションではパズルを解くようなロジックを書くことは稀なので、こういったテクニックの存在は知っていても、なかなか使う機会はないのではないでしょうか。

とはいえ、この手のテクニックの引き出しはできるだけ多いにこしたことはありませんし、来るべきメニーコア時代の並列プログラミングでは必須のテクニックになりそうな予感もあります。このため、機会があれば使って慣れておくのが得策ですが、これに適した普段使いできるテクニックが欲しいところです。

問題

関数fは引数のIntをそのまま返す関数です。実行の確認をするためにprintln関数で、受け取ったIntをコンソールに表示します。

val f = (x: Int) => {
  println(x)
  x
}

次に、Seq[Int]を引数に取り、関数を適用した結果が条件似合うものが見つかったら、計算前の値を返すという関数legacyを定義します。

ループ内でif式で条件判定してreturnで強制的に関数から脱出しています。returnによる強制復帰は手続き型としてはごく普通の書き方です。

def legacy(xs: Seq[Int], f: Int => Int): Option[Int] = {
  for (x <- xs) {
    if (f(x) == 3) return Some(x)
  }
  return None
}

実行結果は以下になります。

scala> legacy(List(1, 2, 3, 4, 5), f)
1
2
3
res3: Option[Int] = Some(3)

関数型

Scalaで関数型的なプログラミングに慣れてくると、returnで強制脱出するようなコーディングに違和感が出てきます。そして、コンビネータを使った以下のようなコーディングを多用するようになります。

scala> List(1, 2, 3, 4, 5).map(f).find(_ == 3)
1
2
3
4
5
res33: Option[Int] = Some(3)

実行の結果無事、正しい結果が帰ってきました。コーディングも簡潔なので万々歳に思えますが、問題がひとつあります。

正しい結果を返すということに関して、List内の4, 5を関数fで評価することは不要ですが、上記の処理では評価が行われています。この例は要素数が5つしかないので実用上は問題ありませんが、1万件のデータに対して3件目で条件がヒットするにもかかわらず、残り9997件の評価が行われるようになってしまうとなると、これはちょっとした事件です。

遅延評価

ここで登場するのが遅延評価です。

ListをStreamに変えると、Steram内の要素に対してmapメソッドで関数fが適用されるタイミングが変わり、不要な要素に対する評価が行われないようになります。

scala> Stream(1, 2, 3, 4, 5).map(f).find(_ == 3)
1
2
3
res34: Option[Int] = Some(3)

関数legacyと同様の動きですね。要素1, 2, 3への評価は行われるものの、要素4, 5への評価は行われずに済みました。

Streamは、ListやVectorと同様にSeqなので使い方は難しくありません。普通にSeqとして使っていけばよいわけですが、コンビネータで値が評価されるタイミングが事前一括評価ではなく、必要時の個別評価になる点が異なります。

今回は普段使いのプログラミングでこの性質を利用するパターンを見つけました。他にも色々あるはずなので、うまくパターンとして採取していきたいと思います。

諸元

  • Scala 2.10.0-RC1

2012年11月5日月曜日

Scala Tips / case classによるDSL

前回取り上げたNamed and Default Argumentsですが、DSLにも大きな影響があります。

ScalaのDSLというと、ScalaTestなどが提供している以下のようなDSLを思い浮かべます。

class StackSpec extends FlatSpec with ShouldMatchers {

  "A Stack" should "pop values in last-in-first-out order" in {
    val stack = new Stack[Int]
    stack.push(1)
    stack.push(2)
    stack.pop() should equal (2)
    stack.pop() should equal (1)
  }

  it should "throw NoSuchElementException if an empty stack is popped" in {
    val emptyStack = new Stack[String]
    evaluating { emptyStack.pop() } should produce [NoSuchElementException]
  }
}

こういった華麗なDSLの問題は、開発コストが結構かかるという点です。また、Scalaの物理的な文法に沿いながらも、独自の文法を編み出すということでもあるので、利用者側の学習コストも馬鹿になりません。

ScalaTest級の大物フレームワークの場合は、こういったところに力を入れても得るところが大きいですが、ちょっとしたマイ・ローカル・プログラムではなかなかこういう所にコストを掛けるのも大変です。

そこで、ボクが最近愛用しているのが、地味にcase classを使う方法です。

たとえば、こういうcase classを定義します。

case class Config(
  name: String,
  version: String = "1.0",
  drivers: Seq[Driver] = Nil)

case class Driver(
  name: String,
  url: String,
  params: Map[String, String] = Map.empty)
  Driver("google", "http://www.google.com")))

これを、こういう感じでDSLに使います。

Config("foo", drivers = List(
  Driver("yahoo", "http://www.yahoo.com"),
  Driver("google", "http://www.google.com")))

Named and Default Argumentsの機能を活用することで、不要なパラメタ設定を減らすことができるのが魅力的です。2.8以前はこういうことができなかったので、case classでDSLを作ることのメリットが限定的だったのですが、最新仕様では状況がかわっているというわけです。

細かいですが、以下のようにcase classをtoStringで文字列化した時に、データの内容が分かりやすく整形されるのは、デバッグ時にうれしい機能です。

scala> Config("foo", drivers = List(
     |   Driver("yahoo", "http://www.yahoo.com"),
     |   Driver("google", "http://www.google.com")))
res2: Config = Config(foo,1.0,List(Driver(yahoo,http://www.yahoo.com,Map()), Driver(google,http://www.google.com,Map())))

このDSLは、case classの特徴を引き継いでおり代数的データ型と永続データ構造の性質を合わせ持つ不変オブジェクトでもあるので、内部データ構造としてもそのまま使うことができます。前回紹介したcopy constructorも強い味方です。

華麗なDSLの場合、内部処理用のデータ構造に情報を転記しなければならないことになりがちなので、その作業が不要になるのはかなり大きなメリットです。

諸元

  • Scala 2.10.0-RC1

2012年11月2日金曜日

Scala Tips / case classのcopy constructor

ちょっと旧聞になりますがScala 2.8(2009年)で導入されたNamed and Default Argumentsは地味ですが非常にインパクトのある機能でした。Named and Default Argumentsと同時にCompiler-generated copy methods、いわゆるcopy constructorも導入されました。

これらの機能の導入によってcase classの価値が大幅に向上したといえます。

たとえば、以下のcase class Personがあるとします。これは何の変哲もないcase classですね。

scala> case class Person(name: String, age: Int, phone: String)
defined class Person

Personのインスタンスは以下のように生成します。

scala> val a = Person("Taro", 30, "123-456-7890")
a: Person = Person(Taro,30,123-456-7890)

さて、このPersonの年令を30から35に変更するとします。Personの実装は不変オブジェクトなので値を直接変更することはできないので、値を変更した新しいオブジェクトを再作成します。

普通に考えるとこの処理は以下のようになります。この方法の問題点は、case classの定義する変数をすべて並べて再設定しなければならないことです。この例では、変数の数が3つなのでたいしたことはありませんが、実際のプログラムでは10個ぐらい並ぶのはざらなのですし、データベースのレコードを引き写したcase classだと100個(100カラム)になるかもしれません。こうなってくるとプログラミング時に手で記述していくのは苦行になってしまいます。

scala> val b = Person(a.name, 35, a.phone)
b: Person = Person(Taro,30,123-456-7890)

そこで登場するのがcase classに自動的に追加されるcopyメソッドです。

以下のように値を変更するパラメタのみを指定してcopyメソッドを呼び出すと、指定された値が更新された新しいcase classインスタンスを得ることができます。

scala> val b = a.copy(age = 35)
b: Person = Person(Taro,35,123-456-7890)

copyメソッドを使うことで、不変オブジェクトの一部を更新する永続データ構造系のテクニックをcase classで簡単に使えるようになります。

このあたりのテクニックについて、以前書いたものを調べてみたら、今回の内容とかなり近しいものが見つかりました。視点がちょっと違うのでよしとしましょう。

諸元

  • Scala 2.10.0-RC1

2012年11月1日木曜日

Scala Tips / flatMapとbind(>>=)の違い

ScalaのflatMapメソッドは、モナドのbindに対応するメソッドとして認識されています。つまり、基本的にはScalazの>>=メソッドと同じ動作をするわけですが、微妙な機能差があります。

以下はListに対してflatMapメソッドを用いてモナドのbind処理を行ったものです。

scala> List(1, 2, 3).flatMap(x => if (x % 2 == 0) List(x, x) else Nil)
res16: List[Int] = List(2, 2)

これはScalazの>>=メソッドも全く同じ動作をします。

scala> List(1, 2, 3) >>= (x => if (x % 2 == 0) List(x, x) else Nil)
res17: List[Int] = List(2, 2)

次の例

さて、今度の例はListのコンテナに対してOptionをflatMap関数で適用しています。これも想定通りの動作をしました。

scala> List(1, 2, 3).flatMap(x => if (x % 2 == 0) Option(x) else None)
res18: List[Int] = List(2)

しかし、Scalazの>>=メソッドでは文法エラーになってしまいました。

scala> List(1, 2, 3) >>= (x => if (x % 2 == 0) Option(x) else None)
<console>:14: error: type mismatch;
 found   : Option[Int]
 required: List[?]
              List(1, 2, 3) >>= (x => if (x % 2 == 0) Option(x) else None)
                                                            ^
<console>:14: error: type mismatch;
 found   : None.type
 required: List[?]
              List(1, 2, 3) >>= (x => if (x % 2 == 0) Option(x) else None)

なぜ、このような結果になってしまうのでしょうか。

「モナドは単なる自己関手の圏におけるモノイド対象だよ。」という有名な?説明がありますが、モナドは俗っぽい言い方をするとコンテナ(モナド)の中にコンテナが入っている構造で、外側のコンテナと内側のコンテナが同じ型の時に一つにまとめる事ができるものです。

ここで重要なのは、外側のコンテナと内側のコンテナが同じ型でなければならないという前提条件です。「List(1, 2, 3) >>= (x => if (x % 2 0) Option(x) else None)」が文法エラーになってしまうのは、外側のコンテナがList、内側のコンテナがOptionで、(各々はモナドではあるものの)型が違うためですね。

Scalazの>>=メソッドはモナドのbindとしては正しい動作になっているわけです。

flatten

ScalaではListなどのコレクションはflattenメソッドを提供しています。flattenメソッドは、外側のコンテナと内側のコンテナの型の相違は気にせず、以下のように内側のコンテナを平坦化する処理を行います。

scala> List(None, Some(2), None).flatten
res20: List[Int] = List(2)

これをflatMapメソッドの動作と同じになるようにしてみると以下になります。flatMapメソッドは文字通りmapした後にflattenする処理を行うわけですね。

scala> List(1, 2, 3).map(x => if (x % 2 == 0) Option(x) else None).flatten
res23: List[Int] = List(2)

flatMapメソッドは基本的にはモナドのbindと考えておいてよいですが、flatMapメソッドの適用範囲がモナドより少し広いことを知っておくとプログラミングの幅が広がります。

特に、外側のコンテナがList(といったSeq)、内側のコンテナがOptionの組合せはScalaプログラミングでは頻出なので、flatMapメソッド(あるいはflattenメソッド)でこの組合せを捌く方法はScalaプログラマの必須イディオムといえます。

諸元

  • Scala 2.10.0-RC1
  • Scalaz 2.10.0-M6

2012年10月31日水曜日

Scala Tips / ケースクラス設計原則

重厚な名前をつけてみましたが、内容はゆるふわです。

Scalaには色々な革新的な機能が導入されていますが、あえて日々のプログラミングで一番重要な機能は?と考えていくとケースクラスですよね。

ケースクラスは便利なので、なんにでも使いたくなってしまいますが、最近原則めいたことが分かってきたので、日々のプログラミング時の個人的な指針としています。

ケースクラスは直積

ケースクラスの利用方法を考える上で重要な指針となるのは、ケースクラスは「直積」という点です。Scalaでは直積はProductというトレイトで表現しますが、ケースクラスもこのProductが自動的にmixinされます。

また、直積に関連して代数的データ型という観点も重要です。このあたりの事情は「クラウド温泉3.0 (3) / 代数的データ型 on Scala」にも書きました。

原則

直積、代数的データ構造といった数学的な構成要素がケースクラスの土台になっているすると、ケースクラスではこの土台を揺るがすようなコーディングは慎むのが安全策です。

土台を揺るがす要因は以下の2点です。

  • 更新可能な変数を持っている
  • インヘリタンスで後付で振る舞いの変更が可能

これを避けるためには、ケースクラスは以下の使い方に留めるのが安全策となります。

  • ケースクラスは不変オブジェクトの実装に用いる。
  • インヘリタンスはsealedを用いて適用範囲を限定する。

つまり、ケースクラスは純粋関数型の範囲で使うということですね。

逆に、ケースクラス的な使い方をするクラスでも、不変オブジェクトではない場合はあえてケースクラスにしないというコーディングをします。こうすることによって、このコーディング方針を知っていれば、プログラムの意図が明確になりプログラムの可読性があがります。

以上、ゆるふわ指針でした。

2012年10月30日火曜日

Scala Tips / Option(18) - Boolean

昨日に引き続いてScalazの小ネタです。

OptionとBooleanの相互変換もよく出てくる処理です。普通に考えても簡単にできる処理ではありますが、より簡潔に書くことができればプログラミング効率も地味に上がってきます。

Option→Boolean

まず、OptionをBooleanに変換する処理です。OptionがSomeの場合はtrue、Noneの場合はfalseを得る処理になります。

この手の処理はmatch式が定番です。

o match {
  case Some(_) => true
  case None => false
}

しかし、Scalazを使うともう少し短くできそうです。

まずcataメソッド。

o.cata(x => true, false)

次はsomeメソッドとnoneメソッドのチェイン。

o.some(x => true).none(false)

少し短くすることができましたが、Someに格納されている値は無関係なのでもう少し短くしたいところです。

そこで登場するのが?メソッドと|メソッドのチェイン。

o ? true | false

これはかなり短くなりました。

と、ここでよく考えてみると、Scalazを使わないでもOptionにそのものズバリがありました。

o.isDefined

OptionからBooleanへの変換はOptionのisDefinedメソッドを使うのが結論です。

Boolean→Option

BooleanをOptionに変換するときには:

if (b) 1.some else none

とするのが普通ですが、Scalazを用いると以下のように書くこともできます。

b ? 1.some | none

いずれの場合も、「else none」や「| none」がちょっと悔しいですね。

Scalazだと以下のように書くことができます。

b option 1

optionメソッドはby-nameパラメタなので以下のように処理を記述することもできます。この使い方は結構使い出があります。

b option {
  ...何かの処理
}

諸元

  • Scala 2.10.0-RC1
  • Scalaz 2.10.0-M6