Haskell 勉強メモ3
型変数
Haskellの head
関数の型を見てみる。
*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
を返す。Ordering
は GT, 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
型クラス
上限と下限を持つ。それぞれ、 minBound
と maxBound
関数で調べることができる。
*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 型クラス
Float
と Double
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
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]
Scalaは if
の真偽値が想定になるようにしてあげればいい
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 Type SQL Type Example SQL Literal java.time.Instant
TEXT
'2019-02-03T18:20:28.660Z'
java.time.LocalDate
DATE
'2019-02-03'
java.time.LocalTime
TEXT
'18:20:28.661'
java.time.LocalDateTime
TEXT
'2019-02-03T18:20:28.661'
java.time.OffsetTime
TEXT
'18:20:28.661Z'
java.time.OffsetDateTime
TEXT
'2019-02-03T18:20:28.661Z'
java.time.ZonedDateTime
TEXT
'2019-02-03T18:20:28.661Z[Europe/London]'
H2
Java Type SQL Type Example SQL Literal java.time.Instant
TIMESTAMP(9) WITH TIME ZONE
'2019-02-03T18:20:28.660Z'
java.time.LocalDate
DATE
'2019-02-03'
java.time.LocalTime
VARCHAR
'18:20:28.661'
java.time.LocalDateTime
TIMESTAMP
'2019-02-03 18:20:28.661'
java.time.OffsetTime
VARCHAR
'18:20:28.661Z'
java.time.OffsetDateTime
VARCHAR
'2019-02-03T18:20:28.661Z'
java.time.ZonedDateTime
VARCHAR
'2019-02-03T18:20:28.661Z[Europe/London]'
Postgres
Java Type SQL Type Example SQL Literal java.time.Instant
TIMESTAMP
'2019-02-03 18:20:28.66'
java.time.LocalDate
DATE
'2019-02-03'
java.time.LocalTime
TIME
'18:20:28.661'
java.time.LocalDateTime
TIMESTAMP
'2019-02-03 18:20:28.661'
java.time.OffsetTime
TIMETZ
'18:20:28.661Z'
java.time.OffsetDateTime
VARCHAR
'2019-02-03T18:20:28.661Z'
java.time.ZonedDateTime
VARCHAR
'2019-02-03T18:20:28.661Z[Europe/London]'
DB2
Java Type SQL Type Example SQL Literal java.time.Instant
VARCHAR(254)
'2019-02-03T18:20:28.660Z'
java.time.LocalDate
DATE
'2019-02-03'
java.time.LocalTime
VARCHAR(254)
'18:20:28.661'
java.time.LocalDateTime
TIMESTAMP
'2019-02-03 18:20:28.661'
java.time.OffsetTime
VARCHAR(254)
'18:20:28.661Z'
java.time.OffsetDateTime
VARCHAR(254)
'2019-02-03T18:20:28.661Z'
java.time.ZonedDateTime
VARCHAR(254)
'2019-02-03T18:20:28.661Z[Europe/London]'
DerbyProfile
java TYPE SQL type example sql literal java.time.Instant
VARCHAR(254)
'2019-02-03T18:20:28.660Z'
java.time.LocalDate
DATE
'2019-02-03'
java.time.LocalTime
VARCHAR(254)
'18:20:28.661'
java.time.LocalDateTime
TIMESTAMP
'2019-02-03 18:20:28.661'
java.time.OffsetTime
VARCHAR(254)
'18:20:28.661Z'
java.time.OffsetDateTime
VARCHAR(254)
'2019-02-03T18:20:28.661Z'
java.time.ZonedDateTime
VARCHAR(254)
'2019-02-03T18:20:28.661Z[Europe/London]'
Oracle
Java Type SQL Type Example SQL Literal java.time.Instant
TIMESTAMP(9) WITH TIME ZONE
TO_TIMESTAMP_TZ('2019-02-03 18:20:28.660 +00', 'YYYY-MM-DD HH24:MI:SS.FF3 TZH')
java.time.LocalDate
DATE
TO_DATE('2019-02-03', 'SYYYY-MM-DD')
java.time.LocalTime
VARCHAR2(254)
'18:20:28.661'
java.time.LocalDateTime
TIMESTAMP
TO_TIMESTAMP('2019-02-03 18:20:28.661', 'YYYY-MM-DD HH24:MI:SS.FF3')
java.time.OffsetTime
TIMESTAMP(6) WITH TIME ZONE
TO_TIMESTAMP_TZ('1970-01-01 18:20:28.661 +0000', 'YYYY-MM-DD HH24:MI:SS.FF3 TZH:TZM')
java.time.OffsetDateTime
TIMESTAMP(6) WITH TIME ZONE
TO_TIMESTAMP_TZ('2019-02-03 18:20:28.661 +0000', 'YYYY-MM-DD HH24:MI:SS.FF3 TZH:TZM')
java.time.ZonedDateTime
TIMESTAMP(6) WITH TIME ZONE
TO_TIMESTAMP_TZ('2019-02-03 18:20:28.661 Europe/London', 'YYYY-MM-DD HH24:MI:SS.FF3 TZR')
SQLite
Java Type SQL Type Example SQL Literal java.time.Instant
TIMESTAMP
1549218028660
java.time.LocalDate
DATE
1549152000000
java.time.LocalTime
VARCHAR(254)
'18:20:28.661'
java.time.LocalDateTime
TIMESTAMP
1549218028661
java.time.OffsetTime
VARCHAR(254)
'18:20:28.661Z'
java.time.OffsetDateTime
VARCHAR(254)
'2019-02-03T18:20:28.661Z'
java.time.ZonedDateTime
VARCHAR(254)
'2019-02-03T18:20:28.661Z[Europe/London]'
SQLServer
Java Type SQL Type Example SQL Literal java.time.Instant
DATETIMEOFFSET(6)
(convert(datetimeoffset(6), '2019-02-03 18:20:28.66 '))
java.time.LocalDate
DATE
'2019-02-03'
java.time.LocalTime
TIME(6)
(convert(time(6), '18:20:28.661'))
java.time.LocalDateTime
DATETIME2(6)
'2019-02-03 18:20:28.661'
java.time.OffsetTime
VARCHAR(MAX)
'18:20:28.661Z'
java.time.OffsetDateTime
DATETIMEOFFSET(6)
(convert(datetimeoffset(6), '2019-02-03 18:20:28.661 '))
java.time.ZonedDateTime
VARCHAR(MAX)
'2019-02-03T18:20:28.661Z[Europe/London]'
Hsqldb
Java Type SQL Type Example SQL Literal java.time.Instant
TIMESTAMP(9) WITH TIME ZONE
'2019-02-03 18:20:28.66'
java.time.LocalDate
DATE
'2019-02-03'
java.time.LocalTime
TIME(3)
'18:20:28.661'
java.time.LocalDateTime
TIMESTAMP
'2019-02-03 18:20:28.661'
java.time.OffsetTime
TIME(9) WITH TIME ZONE
'18:20:28.661+0:00'
java.time.OffsetDateTime
TIMESTAMP(9) WITH TIME ZONE
'2019-02-03 18:20:28.661+0:00'
java.time.ZonedDateTime
LONGVARCHAR
'2019-02-03T18:20:28.661Z[Europe/London]'
Scala で FizzBuzz
今更だけど、FizzBuzzやってみる。
「1から100までの数字を画面に表示する」「3の倍数のときは数字の代わりにFizzと表示する」「5の倍数のときは数字の代わりにBuzzと表示する」「15の倍数のときは数字の代わりに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 class
の Seq
に対するメソッドを生やしたい時どうするか。例えば、下記のような 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
の追記。コレクションに対して制約がある場合などはコレクションオブジェクトを作るのがいいかなというのが最近の私の方針です。