Haskell 勉強メモ3

型変数

Haskellhead 関数の型を見てみる。

*Main> :t head
head :: [a] -> a

この a型変数と呼ばれる。型変数によって関数を複数の型に対して動作するようにできる。型変数を用いた関数は多相的関数と呼ばれる。

どのような型でも良いという性質のことを多相性(polymorphism)と呼ぶ。型変数を含むような型のことを多相型 と呼ぶ。

型クラス

型クラス (type class) とは、ある型が何らかの性質を持つことを示すインターフェースのこと。型クラスは関数の集まりを定める。型クラスに属する関数のことをその型クラスのメソッドと呼ぶことがある。

等価性を定義する型クラスの例

*Main> :t (==)
(==) :: Eq a => a -> a -> Bool

これは、等価性関数が「同じ型の任意の2つの引数を取り、Boolを返す」関数であることを意味する。

シンボル => よりも前にあるものは型クラス制約と呼ばれ、「引数の2つの値のかたは Eq クラスのインスタンス出なければならない」ことを示す。

型クラスの例

Eq 型クラス

等価性をテストできる型に使われる。関数の型変数に Eq クラスの制約がついていたら、その関数の定義のどこかで、==/= が使われていることを示す。

Ord 型クラス

順序を付けられる型のための型クラス。

*Main> :t (>)
(>) :: Ord a => a -> a -> Bool

引数を2つとり、それらが関係を満たすかどうかを教えてくれる Bool を返す。

compare 関数は Ordインスタンスの型の引数を2つとり、Ordering を返す。OrderingGT, LT, EQ のいずれかをとる。

*Main> :t compare
compare :: Ord a => a -> a -> Ordering
*Main> "Abrakadabra" < "Zebra"
True
*Main> "Abrakadabra" `compare` "Zebra"
LT
*Main> 5 >= 2
True
*Main> 5 `compare` 3
GT
*Main> 'b' > 'a'
True
*Main> 

Show 型クラス

ある値は、その型が Show 型クラスのインスタンスになっていれば、文字列として表現できる。この型クラスの操作でよく使われるのが show で、これは指定した値を文字列として表示する関数。※ print 文みたいなものかな?

*Main> show 3
"3"
*Main> show 5.334
"5.334"
*Main> show True
"True"

Read 型クラス

Show 型と対をなす型クラス。read 関数は文字列を受け取り、Readインスタンスの方を返す。

*Main> :t read
read :: Read a => String -> a

*Main> read "True" || False
True
*Main> read "8.2" + 3.8
12.0
*Main> read "5" - 2
3
*Main> read "[1,2,3,4]" ++ [3]
[1,2,3,4,3]

*Main> read "5" -- Floatなのか、Intなのか、それ以外なのか判断できないためエラー
*** Exception: Prelude.read: no parse
*Main> read "5" :: Float -- 型を明示する必要がある。
5.0
*Main> read "5" :: Int 
5

Enum 型クラス

列挙型。値をレンジの中で使える。

*Main> ['a' .. 'e']
"abcde"
*Main> [LT .. GT]
[LT,EQ,GT]
*Main> [3 .. 5]
[3,4,5]
*Main> succ 'B'
'C'

Bounded 型クラス

上限と下限を持つ。それぞれ、 minBoundmaxBound 関数で調べることができる。

*Main> minBound :: Int
-9223372036854775808
*Main> maxBound :: Int
9223372036854775807
*Main> maxBound :: Char
'\1114111'
*Main> maxBound :: Bool
True
*Main> minBound :: Bool
False

Num型クラス

数の型クラス

*Main> :t 20
20 :: Num p => p

Floating 型クラス

FloatDouble

Integral 型クラス

整数(全体)のみが含まれる数の型クラス。

Haskell 勉強メモ2

明示的な型宣言

Haskellの型宣言は2行で書く模様。こういう書き方は初めて。

removeNonUppercase :: [Char] -> [Char]
removeNonUppercase st = [c | c <- st, c `elem` ['A' .. 'Z']]

*Main> removeNonUppercase("HelloWorld")
"HW"

addThree :: Int -> Int -> Int -> Int
addThree x y z = x + y + z

*Main> addThree 1 2 3
6

Scalaだとこうかな。

def removeNonUppercase(st: Seq[Char]): Seq[Char] = {
  for (c <- st if ('A' to 'Z').contains(c)) yield c
}

removeNonUppercase("HelloWorld") // val res6: Seq[Char] = Vector(H, W)

def addThree(x: Int)(y: Int)(z: Int): Int = x + y + z

addThree(1)(2)(3) // val res7: Int = 6

一般的なHaskellの型

他の言語同様、Int 型がありマシンのワードサイズによって異なる模様。64ビットCPUの場合Intの範囲は -2^63〜2^63-1 なのでScalaだとLong型相当。他にInteger 型があって、これは有界ではなく大きな数値が扱える。BigInt的なものか。

factorial :: Integer -> Integer
factorial n = product [1 .. n]

*Main> factorial 50
30414093201713378043612608166064768844377641568960512000000000000
def factorial(n: BigInt) =  (BigInt(1) to n).foldLeft(BigInt(1)){ (a, b) => a * b}
factorial(BigInt(50)) // val res8: BigInt = 30414093201713378043612608166064768844377641568960512000000000000

FloatとDouble。Floatは単精度浮動小数点数、Doubleは倍精度浮動小数点型。

円の面積を求める関数で試してみる。

circumference :: Float -> Float
circumference r = 2 * pi * r

*Main> circumference 4.0
25.132742

circumference' :: Double -> Double 
circumference' r = 2 * pi * r

*Main> circumference' 4.0
25.132741228718345

Scala

def circumferenceF(r: Float): Float = 2.0f * math.Pi.toFloat * r

def circumferenceD(r: Double): Double = 2.0 * math.Pi * r

circumferenceF(4.0f) // val res9: Float = 25.132742
circumferenceD(4.0) // val res10: Double = 25.132741228718345

他にも

  • 真偽値 Bool (True, False)
  • Char 型(Unicode文字を表し、シングルクォート ' で括って表記する)
  • タブルも型。空のタプル () も型の一つで、ただ一つのあたい () のみをもつ※これはユニットと呼ばれる。

Haskell 勉強メモ 1

関数型プログラミングをちゃんとやろうと思って「すごいHaskellたのしく学ぼう!」を買って読み始めた。その勉強メモ。自分のメイン言語はScalaなので、気になったところを比較しながら見ていく。

内包表記

Haskellのリスト内包表記。集合の内包的記法の概念。$latex \{2\cdot x | x \in N, x \le 10 \}$ を書いてみる。

Prelude> [x*2 | x <- [1..10]]
[2,4,6,8,10,12,14,16,18,20]

Scalaで似たようなものを書く場合は、for 内包表記を使う。

scala> for (i <- 1 to 10) yield i * 2
val res1: IndexedSeq[Int] = Vector(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

もう少し複雑な条件で。

2倍したもののうち、12以上のものからなるリストが欲しい場合

Prelude> [x*2 | x <- [1..10], x*2 >= 12]
[12,14,16,18,20]
scala> for (i <- 1 to 10 if i*2 >= 12) yield i*2
val res3: IndexedSeq[Int] = Vector(12, 14, 16, 18, 20)

10以上の全ての奇数を"BANG!"に置き換え、10より小さい全ての奇数を"BOOM!"に置き換える内包表記。

boomBangs xs = [ if x < 10 then "BOOM!" else "BANG!" | x <- xs, odd x]

// boomBangs [7..13]
// ["BOOM!","BOOM!","BANG!","BANG!"]
def boomBangs(xs: Seq[Int]): Seq[String] = {
  for (x <- xs if x % 2 == 1) yield {
    if (x < 10) "BOOM" else "BANG!"
  }
}

// boomBangs(7 to 13)
// val res1: Seq[String] = Vector(BOOM, BOOM, BANG!, BANG!)

複数の述語を含める場合。Haskellは間まで区切る。and条件

*Main> [ x | x <- [10..20], x /= 13, x /= 15, x /= 19]
[10,11,12,14,16,17,18,20]

Scalaif の真偽値が想定になるようにしてあげればいい

for (x <- 10 to 20 if x != 13 && x != 15 && x !=19) yield x
val res2: IndexedSeq[Int] = Vector(10, 11, 12, 14, 16, 17, 18, 20)

複数のリストから値を取り出す

*Main> [ x+y | x <- [1,2,3], y <- [10,100,1000] ]
[11,101,1001,12,102,1002,13,103,1003]
for (
  x <- 1 to 3;
  y <- Seq(10,100,1000)
) yield x + y

val res3: IndexedSeq[Int] = Vector(11, 101, 1001, 12, 102, 1002, 13, 103, 1003)

述語論理を追加する

*Main> [ x*y | x <- [2,5,10], y <- [8,10,11], x*y > 50]
[55,80,100,110]
for (
  x <- Seq(2,5,10);
  y <- Seq(8,10,11)
  if (x*y > 50)
) yield x*y

直角三角形を見つける

  • 3辺の長さ全て整数
  • 各辺の長さは10以下
  • 周囲の長さは24に等しい

Haskellで書く

// aは斜辺cを超えないかつ、bはaを超えない
triples = [(a, b, c) | c <- [1 .. 10], a <- [1 .. c], b <- [1 .. a], a ^ 2 + b ^ 2 == c ^ 2, a + b + c == 24]
// [(8,6,10)]

Scalaで書く。Scalaだと ^ みたなメソッドはないから、implicit class でちょろっと作って使ってみる

implicit class MyInt(value: Int) {
  def **(n: Int):Int = {
    n match {
      case 0 => 1
      case k => value * **(k-1)
    }
  }
}

val x:Int = 2 ** 5 // 32

for (
  c <- 1 to 10;
  a <- 1 to c;
  b <- 1 to a
  if a ** 2 + b ** 2 == c ** 2
  if a + b + c == 24
) yield (a,b,c)

val res5: IndexedSeq[(Int, Int, Int)] = Vector((8,6,10))

全体的にHaskellの方が数学チックな表現ができるなって印象。Scalaと対比でみることで、Scalaの理解が進みそう。

階層構造のデータから特定のプロパティを抽出して、フラットにする

json のような階層構造を持つデータから特定のプロパティを再帰的に探索して、平坦なリストにしたものを取得したい場面に出会ったのでメモする。

データ構造

case class Tree(
    value: Int,
    children: List[Tree]
)

木構造?というか、自身のリストを持つようなデータ構造。

実装

再帰でなんとかする

case class Tree(value: Int, children: List[Tree]) {
  val values: List[Int] = {
    @tailrec
    def loop(children: List[Tree], vs: List[Int]): List[Int] = {
      children match {
        case Nil => vs
        case ::(head, next) => loop(next ++ head.children, vs appended  head.value)
      }
    }
    loop(children, List(value))
  }
}

実行結果

val tree = {
  Tree(
    1,
    List(
      Tree(2, List.empty),
      Tree(3, List.empty),
      Tree(4, List(
        Tree(5, List.empty),
        Tree(6, List.empty),
        Tree(7, List(
          Tree(8, List(
            Tree(9, List(
              Tree(12, List(
                Tree(13, List.empty)
              ))
            ))
          )),
          Tree(10, List(
            Tree(11, List.empty)
          ))
        ))
      ))
    )
  )
}

println(tree.values) // List(1, 2, 3, 4, 5, 6, 7, 8, 10, 9, 11, 12, 13)</pre>

うん。できてそう。

ちゃんとテストしたわけではない。

問題点

自身のプロパティにアクセスするような実装のため、ほぼ同じ処理なのに共通化しづらいのが難点。再帰的にとってくるような処理をあっちこっちでやると同じ処理が散らばる。

Slick の java.time 対応

普段 PlayFramework + Scala で開発していて、ORMには Slick を使うことが多い。Slickは version 3.3 から java.time をサポートするようになったのだが、MySQL の型との対応がひどく(ほとんどTEXTにマッピングされて使い物にならない)、DBにMySQLを使う場合には気をつけなければならない。この対応表が私の知る限り、SlickのUpgrade Guidesにしか見当たらなくていつも見失うので、メモしておく。

http://scala-slick.org/doc/3.3.3/upgrade.html#support-for-java.time-columns

MySQL

Java TypeSQL TypeExample SQL Literal
java.time.InstantTEXT'2019-02-03T18:20:28.660Z'
java.time.LocalDateDATE'2019-02-03'
java.time.LocalTimeTEXT'18:20:28.661'
java.time.LocalDateTimeTEXT'2019-02-03T18:20:28.661'
java.time.OffsetTimeTEXT'18:20:28.661Z'
java.time.OffsetDateTimeTEXT'2019-02-03T18:20:28.661Z'
java.time.ZonedDateTimeTEXT'2019-02-03T18:20:28.661Z[Europe/London]'
slick.jsbc.MySQLProfile

H2

Java TypeSQL TypeExample SQL Literal
java.time.InstantTIMESTAMP(9) WITH TIME ZONE'2019-02-03T18:20:28.660Z'
java.time.LocalDateDATE'2019-02-03'
java.time.LocalTimeVARCHAR'18:20:28.661'
java.time.LocalDateTimeTIMESTAMP'2019-02-03 18:20:28.661'
java.time.OffsetTimeVARCHAR'18:20:28.661Z'
java.time.OffsetDateTimeVARCHAR'2019-02-03T18:20:28.661Z'
java.time.ZonedDateTimeVARCHAR'2019-02-03T18:20:28.661Z[Europe/London]'
slick.jdbc.H2Profile

Postgres

Java TypeSQL TypeExample SQL Literal
java.time.InstantTIMESTAMP'2019-02-03 18:20:28.66'
java.time.LocalDateDATE'2019-02-03'
java.time.LocalTimeTIME'18:20:28.661'
java.time.LocalDateTimeTIMESTAMP'2019-02-03 18:20:28.661'
java.time.OffsetTimeTIMETZ'18:20:28.661Z'
java.time.OffsetDateTimeVARCHAR'2019-02-03T18:20:28.661Z'
java.time.ZonedDateTimeVARCHAR'2019-02-03T18:20:28.661Z[Europe/London]'
slick.jdbc.PostgresProfile

DB2

Java TypeSQL TypeExample SQL Literal
java.time.InstantVARCHAR(254)'2019-02-03T18:20:28.660Z'
java.time.LocalDateDATE'2019-02-03'
java.time.LocalTimeVARCHAR(254)'18:20:28.661'
java.time.LocalDateTimeTIMESTAMP'2019-02-03 18:20:28.661'
java.time.OffsetTimeVARCHAR(254)'18:20:28.661Z'
java.time.OffsetDateTimeVARCHAR(254)'2019-02-03T18:20:28.661Z'
java.time.ZonedDateTimeVARCHAR(254)'2019-02-03T18:20:28.661Z[Europe/London]'
slick.jdbc.DB2Profile

DerbyProfile

java TYPESQL typeexample sql literal
java.time.InstantVARCHAR(254)'2019-02-03T18:20:28.660Z'
java.time.LocalDateDATE'2019-02-03'
java.time.LocalTimeVARCHAR(254)'18:20:28.661'
java.time.LocalDateTimeTIMESTAMP'2019-02-03 18:20:28.661'
java.time.OffsetTimeVARCHAR(254)'18:20:28.661Z'
java.time.OffsetDateTimeVARCHAR(254)'2019-02-03T18:20:28.661Z'
java.time.ZonedDateTimeVARCHAR(254)'2019-02-03T18:20:28.661Z[Europe/London]'
slick.jdbc.DerbyProfile

Oracle

Java TypeSQL TypeExample SQL Literal
java.time.InstantTIMESTAMP(9) WITH TIME ZONETO_TIMESTAMP_TZ('2019-02-03 18:20:28.660 +00', 'YYYY-MM-DD HH24:MI:SS.FF3 TZH')
java.time.LocalDateDATETO_DATE('2019-02-03', 'SYYYY-MM-DD')
java.time.LocalTimeVARCHAR2(254)'18:20:28.661'
java.time.LocalDateTimeTIMESTAMPTO_TIMESTAMP('2019-02-03 18:20:28.661', 'YYYY-MM-DD HH24:MI:SS.FF3')
java.time.OffsetTimeTIMESTAMP(6) WITH TIME ZONETO_TIMESTAMP_TZ('1970-01-01 18:20:28.661 +0000', 'YYYY-MM-DD HH24:MI:SS.FF3 TZH:TZM')
java.time.OffsetDateTimeTIMESTAMP(6) WITH TIME ZONETO_TIMESTAMP_TZ('2019-02-03 18:20:28.661 +0000', 'YYYY-MM-DD HH24:MI:SS.FF3 TZH:TZM')
java.time.ZonedDateTimeTIMESTAMP(6) WITH TIME ZONETO_TIMESTAMP_TZ('2019-02-03 18:20:28.661 Europe/London', 'YYYY-MM-DD HH24:MI:SS.FF3 TZR')
slick.jdbc.OracleProfile

SQLite

Java TypeSQL TypeExample SQL Literal
java.time.InstantTIMESTAMP1549218028660
java.time.LocalDateDATE1549152000000
java.time.LocalTimeVARCHAR(254)'18:20:28.661'
java.time.LocalDateTimeTIMESTAMP1549218028661
java.time.OffsetTimeVARCHAR(254)'18:20:28.661Z'
java.time.OffsetDateTimeVARCHAR(254)'2019-02-03T18:20:28.661Z'
java.time.ZonedDateTimeVARCHAR(254)'2019-02-03T18:20:28.661Z[Europe/London]'
slick.jdbc.SQLiteProfile

SQLServer

Java TypeSQL TypeExample SQL Literal
java.time.InstantDATETIMEOFFSET(6)(convert(datetimeoffset(6), '2019-02-03 18:20:28.66 '))
java.time.LocalDateDATE'2019-02-03'
java.time.LocalTimeTIME(6)(convert(time(6), '18:20:28.661'))
java.time.LocalDateTimeDATETIME2(6)'2019-02-03 18:20:28.661'
java.time.OffsetTimeVARCHAR(MAX)'18:20:28.661Z'
java.time.OffsetDateTimeDATETIMEOFFSET(6)(convert(datetimeoffset(6), '2019-02-03 18:20:28.661 '))
java.time.ZonedDateTimeVARCHAR(MAX)'2019-02-03T18:20:28.661Z[Europe/London]'
slick.jdbc.SQLServerProfile

Hsqldb

Java TypeSQL TypeExample SQL Literal
java.time.InstantTIMESTAMP(9) WITH TIME ZONE'2019-02-03 18:20:28.66'
java.time.LocalDateDATE'2019-02-03'
java.time.LocalTimeTIME(3)'18:20:28.661'
java.time.LocalDateTimeTIMESTAMP'2019-02-03 18:20:28.661'
java.time.OffsetTimeTIME(9) WITH TIME ZONE'18:20:28.661+0:00'
java.time.OffsetDateTimeTIMESTAMP(9) WITH TIME ZONE'2019-02-03 18:20:28.661+0:00'
java.time.ZonedDateTimeLONGVARCHAR'2019-02-03T18:20:28.661Z[Europe/London]'
slick.jdbc.HsqldbProfile

Scala で FizzBuzz

今更だけど、FizzBuzzやってみる。

「1から100までの数字を画面に表示する」「3の倍数のときは数字の代わりにFizzと表示する」「5の倍数のときは数字の代わりにBuzzと表示する」「15の倍数のときは数字の代わりにFizzBuzzと表示する」プログラム

フィズバズ問題 (FizzBuzz問題)

なんか、Twitterで時々出てきてなんだろうと思ってたけど、思ったより単純な問題だった。

シンプルに。Scala で書くならパターンマッチで書くのがデフォかな。

def fizzBuzz(n: Int): String = {
  n match {
    case fbzz if fbzz % 15 == 0 => "FizzBuzz"
    case fizz if fizz % 3 == 0  => "Fizz"
    case buzz if buzz % 5 == 0  => "Buzz"
    case num => num.toString
  }
}

(1 to 100).map(fizzBuzz).foreach(println)</pre>

これだけ。少し気になった。

implicit class を利用して Seq (とか)にメソッドを生やす

case classSeq に対するメソッドを生やしたい時どうするか。例えば、下記のような case class があるとする。

case class Range(
    min: Int,
    max: Int
)

これの Seq つまり、Seq[Range] に対して、含まれるRangeの中から、最小の min と 最大の max を取得し、それを Range として欲しい時どうするか。利用箇所が1箇所なら、普通に map とかで頑張って処理しちゃえばいい気もするが、複数箇所で利用したり、そもそもコードがごちゃつくので少し避けたい。かといって、下記のように、それ用のメソッドを用意するのもやりすぎ感ある。

object Range {
  def minMaxRange(values: Seq[Range]): Range = {
    val min = values.map(_.min).min
    val max = values.map(_.max).max

    Range(min, max)
  }
}
val ranges = Seq(
  Range(1, 10),
  Range(2, 9),
  Range(0, 3),
  Range(8, 11)
)

Range.minMaxRange(ranges)</pre>

呼び出し元も、少し長くなって読みづらい。

結論

case class のコンパニオンオブジェクトに implicit class を作成して使う。

case class Range(
    min: Int,
    max: Int
)

object Range {
  implicit class RangesOps(values: Seq[Range]) {
    def minMaxRange: Range = {
      require(values.nonEmpty)
      val min = values.map(_.min).min
      val max = values.map(_.max).max
      Range(min, max)
    }
  }
}

こうすることで、Seqに対して定義済みの値があるように使え、呼び出しもとが簡潔になる。

val ranges = Seq(
  Range(1, 10),
  Range(2, 9),
  Range(0, 3),
  Range(8, 11)
)

ranges.minMaxRange   // val res0: Range = Range(0,11)

PreScalaMatsuri で似たような発表があったけど、その時はWrapperクラスを用意する方針の話だったかな。。?個人的には無駄で意味のないクラスはできる限り増やしたくないので、現状は implicit class の利用がいいかなと思ってる。

追記

ラッパークラスを用意する方針は、「コレクションオブジェクト」あるいは「ファーストクラスコレクション」というテクニックに該当することを後で知りました。 ちょっとしたコレクション処理の追加ならばコンパニオンオブジェクトへの implicit class の追記。コレクションに対して制約がある場合などはコレクションオブジェクトを作るのがいいかなというのが最近の私の方針です。