Skip to content

Instantly share code, notes, and snippets.

@gakuzzzz
Last active June 11, 2019 01:59
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gakuzzzz/c747dc7262b3c7181c13 to your computer and use it in GitHub Desktop.
Save gakuzzzz/c747dc7262b3c7181c13 to your computer and use it in GitHub Desktop.
パターンマッチいろいろ (函数型なんたらの集い 2014 in Tokyo)

パターンマッチいろいろ

  • 2014/10/25 函数型なんたらの集い 2014 in Tokyo
  • @gakuzzzz
  • 中村 学
  • 株式会社Tech to Value
  • Scalaから来ました

参加の経緯

https://twitter.com/bleis/status/512053257351348224 https://twitter.com/gakuzzzz/status/512071190035173377

函数型とパターンマッチ

特に依存しているわけではありません。

パターンマッチを持たない函数型言語もあれば、パターンマッチを持つ非函数型言語もあるでしょう。

パターンマッチが無ければ函数型言語ではない、とも言えません。

でも親和性高いし、なんだかんだで函数型言語では古くから一般的なので、同一コンテキストで語られることが良くあります。

パターンマッチとは

こんなの

例1 match 式でパターンマッチ

val userId: Option[Int] = Some(100)

val result = userId match {
  case Some(id) => s"user: $id is logged in"
  case None     => s"not be logged in"
}

println(result) // user: 100 is logged in

例2 変数宣言でパターンマッチ

def duplicate(a: Int): (Int, Int) = (a, a)
val (foo, bar) = duplicate(10)

println(foo)  // 10
println(bar)  // 10

例3 for 式でパターンマッチ

val list = Seq(Some(10), None, Some(3), Some(5), None)

val result = for {
  Some(value) <- list
} yield value * 2

println(result) // List(20, 6, 10)

パターンマッチのメリット

  • ネストした複雑な条件を簡単に表現
  • 値の判定と取得を不可分に

ネストした複雑な条件を簡単に表現

赤黒木の rotate の例

import tree.{Node => N, Red => R, Black => B}
private val balance: (Color, RBTree, Entry, RBTree) => Node = {
  case (B, N(R, N(R, a, x, b), y, c), z, d) => N(R, N(B, a, x, b), y, N(B, c, z, d))
  case (B, N(R, a, x, N(R, b, y, c)), z, d) => N(R, N(B, a, x, b), y, N(B, c, z, d))
  case (B, a, x, N(R, N(R, b, y, c), z, d)) => N(R, N(B, a, x, b), y, N(B, c, z, d))
  case (B, a, x, N(R, b, y, N(R, c, z, d))) => N(R, N(B, a, x, b), y, N(B, c, z, d))
  case (c, l, e, r) => N(c, l, e, r)
}

値の判定と取得を不可分に

よく見る Java7 以前の例

final Optional<String> userNameOpt = Optional.from("gakuzzzz");

if (!userNameOpt.isPresent()) return;
final String userName = userNameOpt.get();  // こわい

仮にメソッドが長くなって分割しよう、とかした時に、うっかり ifget() を分けてしまい、if 通らずに get() の処理に流れるバグを仕込んでもコンパイル通ってしまいます。

_人人人人人人人人人人人人人人人人_
> 実行時NoSuchElementException <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

パターンマッチの場合は値の判定と取得が不可分なので、この手のうっかりミスを入れようがありません。

パターンの拡張

Scalaではユーザがパターンを独自に追加できます。

DateTime から年と月と日を取得する例

object YMD {
  def unapply(d: DateTime): Option[(Int, Int, Int)] = {
    Some((d.getYear, d.getMonth, d.getDayOfMonth))
  }
}

def dateString(d: DateTime): String = d match {
  case YMD(_,    12, 25) => "クリスマス",
  case YMD(2014, 10, 25) => "函数型なんたら2014"
  case _                 => "ふつーの日"
}

値を取り出す個数が不定の場合は、タプルの代わりに Seq が使える unapplySeq というメソッドでもOKです。

Scala ではこういう unapply/unapplySeq を持ったオブジェクトを Extractor と呼んだりします。Constructor との対比ですね。

Seq のパターンマッチでよく使われる +:+: オブジェクトが unapply を持っている ので書くことができます。

def sum(seq: Seq[Int]): Int = {
  seq match {
    case head +: tail => head + sum(tail)
    case _            => 0
  }
}

面白い実例としては、 unfiltered というライブラリが提供している & という Extractor です。

object & {
  def unapply[A](a: A): Option[(A, A)] = Some((a, a))
}

これは単純に評価するオブジェクトを複製して返します。

これがあると、複数のパターンにマッチするパターンというのが簡単にかけるようになります。

def dateString(d: DateTime): String = d match {
  case YMD(2014, 10, _) & HMS(15, 20, _) => ...
  ...
}

Haskell(GHC) の View Patterns

GHC 構文拡張の View Patterns を使うことで、似たような事を実現できます。

{-# LANGUAGE ViewPatterns #-}
import Data.Time.Calendar

dateString :: Day -> String
dateString (toGregorian -> (_,    12, 25)) = "クリスマス"
dateString (toGregorian -> (2014, 10, 25)) = "函数型なんたら2014"
dateString _                               = "ふつーの日"

Clojure の unapply ライブラリ

Clojure だと core.match が標準でありますが、これはユーザが独自にパターンを追加することができません(よね?間違ってたら指摘頂けると助かります)。

そこで、ねこはる先生が unapply というライブラリを作成されました。

このライブラリを使うと、ユーザ定義のパターンを使ってパターンマッチができるようになります。

(defn ymd [d]
  [(year d) (month d) (day d)])

(defn date-string [d]
  (match d
    (ymd _     12 25) "クリスマス"
    (ymd 21014 10 25) "函数型なんたら2014"
    _                 "ふつーの日"))

Scalaやその他言語のパターンマッチの問題点

ソースコード上でパターン表現する関係上、どうしても順序が生まれてしまう、という問題があります。

例えば、要素の順序に意味を持たない Map[K, V] みたいなデータ構造で、パターンマッチを行いたい時を考えてみてください。

object M {
  def unapplySeq[K, V](map: Map[K, V]): Option[Seq[(K, V)]] = Some(map.toSeq)
}

// 意図した通りには動かない!
def test(map: Map[Int, String]): String = map match {
  case M((_, v1), (_, v2), _:*) if v1 == v2 => "2つのkeyで同じ値を持っている"
  case M((10, _), _:*)                      => "10というkeyを持っている"
  case _                                    => ""
}

みたいな事をやりたいけれど、Mapが順序に意味を持たないため、単純に toSeq をしてもどういう順序で並んでいるか予想がつきません。

また、仮に key が Int などの順序付けできる型だとして、結果の Seq を key の順でソートできたとしましょう。

その場合でも、任意の二つの key が同じ value を持つ、というのを現在のパターン上で表現するのは難しいです。

def test(map: Map[Int, String]): String = map match {
  case M((_, v1), (_, v2), _:*)                   if v1 == v2 => ...
  case M(_,       (_, v1), (_, v2), _:*)          if v1 == v2 => ...
  case M(_,       _,       (_, v1), (_, v2), _:*) if v1 == v2 => ...
  ...
}

Clojure は Map に対するパターンマッチを標準で持っていて便利ですね。

Egison の紹介

こういった順序が意味を持たないデータに対して、うまくパターンマッチを書けるようにした言語があります。

Egison

直感をそのまま表現するパターンマッチング

Egison は1つの定まった標準形を持たないデータに対しても柔軟なパターンマッチが表現可能なプログラミング言語です。 リストや多重集合、集合、ツリー、グラフなどといった幅広いデータ型に対して、パターンマッチが記述できます。 それにより、Egison プログラマは非常にシンプルにプログラムを記述できるようになります。

ポーカーの役判定の例

Egison で面白いところは、パターンがマッチした結果として、複数の結果が返ってくる場合も考えられている点ですね。

Scala での文法拡張の提案

くやしいので、Scalaでも似たような事を実現できるような文法拡張を考えてみました。

Scala の extractor 文法の拡張

ニーズが多ければ本家にも積極的に提案していきたいと考えてるので、フィードバックその他ご意見頂けるとうれしいです。

まとめ

パターンマッチは適した場所で使うとコードを簡潔にして可読性、保守性を上げることができます。

上手に使ってハッピーなエンジニアライフを送りましょう。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment