Haskell

Haskellメモ

はじめに

アカウントを作ったので、自分のメモで初めての投稿をしてみます。もしHaskellについて時間をかけずに少し知りたいなら、目を通すと面白いかも。
注意
これは、ウォークスルーHaskell(http://walk.northcol.org/haskell/ )とHaskell超入門(https://qiita.com/7shi/items/145f1234f8ec2af923ef )の自分用メモです。素晴らしい記事に感謝。この記事は上記サイトの序盤の少ししか書いてないです。Haskellをまじめに勉強したい人は、上記のサイトまたは自分で調べて学ぶとよいと思います。

Haskell難しすぎ…。

環境構築

stackでhaskellをインストールし、
stack ghci で対話型実行環境
stack ghc barbar.hs & barbar.exe で実行
lintingが欲しいなら(VS Codeの場合)、stack install hlintでlintツールをインストール(超時間かかる)してから、vscodeに拡張機能haskell-linterを入れる。

main関数

-- main関数がエントリポイント
-- print で任意の型をデバッグ用に出力 (C言語でのint main(){printf("hello, world!"); return 0;}的な)
main = print "Hello, world!"

main = print 2 * 3 -- エラー ∵(print 2) * 3 と解釈される
main = print (2 * 3) -- 6
main = print $ 2 * 3 -- 6  (print (2 * 3) の別の書き方。$は行末までを括弧で囲むことを意味する。)

main = 3 -- エラー。main関数はIO()型を返さないといけない (printはIO()を返す)

{-
  {- ブロックコメント。C言語とかの/* */と違ってネスト可能 -}
-}

doブロック

-- do {a; b; ...} で式をa, b, ...の順に処理する関数を定義
main = do { putStrLn "hello" ; putStrLn "world"}
-- 1行なら{}は省略可
main = do   putStrLn "hello" ; putStrLn "world"
-- インデントをそろえる場合も{}を省略可
main = do putStrLn "hello"
          putStrLn 
              "world" --関数の引数を別の行に改行してもよい

変数

-- 変数の宣言は不要。
-- Haskellの変数は他言語で言う定数で、1つの変数に2度代入することはできない。
-- x'y = 3 のように、変数名に'を含められる。
-- 変数への代入は記述の順番を無視して実行され、代入位置より上でも使える。
-- 1文字目は小文字の英字またはアンダースコアのみ。
main = print n -- 123 を出力
n = 123
-- 関数についても同様。
main = a --"hello" を出力
a = print "hello"

-- 変数と関数の宣言方法は同じなので、haskellの変数は引数を取らない関数とみることもできるかも。
--let{a;b;...}in c や do{let{a; b; ...}; c} や c where{a; b; ...} で変数のスコープを制限。
main = print (area 30)
-- 以下の3つは同じ意味
area r = let {pi = 3.14; square x = x * x} in pi * square r
area r = do {let {pi = 3.14; square x = x * x}; pi * square r}
area r = pi * square r where {pi = 3.14; square x = x * x}

-- do{}のときと同様、let{}やwhere{}などはインデントで表すことができる。(この後のcase ~ of{}なども同様)
area r = do
    let pi = 3.14
        square x = x * x
    pi * square r
-- 変数の型を明示的に指定できるが、ふつう必要ない。
main = do print m; print n; print l
m, n :: Int
m = 1 + 2
n = 1 + 3
l = 3 + 4 ::Int

-- 分割代入あり。
-- ワイルドカード _ への代入は無視される。
main = do print a; print b; print c; print d; print e  -- 123  1.23  "123"  1  2
(a, b, c) = (123, 1.23, "123")
[d, e, _] = [1, 2, 3]

-- -- この式ではtに(123, 3.14, "hello")が入り、a, b, cに各要素が入る。
main = do print t; print a  -- (123, 1.23, "123")  123
t@(a, b, c) = (123, 1.23, "123")

Bool : TrueかFalseを取る (true, falseだとエラー)
Int, Integer : 整数。Integerの方が大きい数値を扱える
Float, Double : 浮動小数点数
() : ユニット型。C言語でのvoid型的な。
(a) : 型aと同じ。たとえばIntは(Int)と書いてもよい
(a1, a2, ...) : タプル型。(123, 'a')など
[a] : リスト型。[1, 2, 3]など
Char : 1文字。'a'や'\n'など
String : [Char]の別名。"abc\n"など
Maybe a : null許容型的なもの。Just a かNothingのどちらかをとる。Just 5 や Nothingなど

(f(a)f aと等しく、f(a, b)はfにタプル(a, b)を渡すのでf a bとは違う。)

演算子

他言語と大体同じ。違う点を下に挙げる。

C言語 Haskell
!a not a
a != b a /= b
a % b mod a b
float(a / b) div a b

f a ba `f` bと書ける。(a `mod` bのように)
累乗は数値 ^ 整数または実数^^整数または実数**実数 (ここでの実数とは、FloatかDouble)

case

-- 構文:
-- case 式 of {パターン1 -> 式1; パターン2 -> 式2, ...}

-- 例
main = do
  print (getValue 0 Nothing)   -- 出力: 0
  print (getValue 0 (Just 5))  -- 出力: 5

getValue maybe =
  case maybe of
    Nothing -> 0
    Just x  -> x       -- "Just x -> x"は"_ -> x"や"otherwise -> x"や"true -> x"でも同じ


-- |でcase式に条件を追加できる
case x of
    Nothing            -> 0
    Just x | x < 0     -> -x -- Just x かつ x < 0
           | otherwise -> x  -- Just x かつ 非 x < 0

if

-- haskellのif文はC言語での三項演算子のような扱い。
main = print $ f 3  -- "ng"
f a = if a == 1 then "ok" else "ng"

関数定義

-- 関数の引数や返り値の型を宣言できる。(これが無いときはすべての型を受け取れる。)
-- 関数名 :: 引数1の型 -> 引数2の型 -> ... -> 返却型
add :: Int -> Int -> Int
add x y = x + y

-- 任意の型を受け取る関数はforallを使って書く。(数学の∀の意味。すべての型a, bについて、引数1は(a->b)の関数, 引数2は...)
map :: forall a b. (a -> b) -> [a] -> [b]
-- forallは省略可能で、以下のようにも書ける。
map :: (a -> b) -> [a] -> [b]


-- 引数で分けて関数を定義できる。
f 0 = 1 -- 0を受け取ったら1を返す
f x = 0 -- 0以外を受け取ったら0を返す
-- |を使って引数の条件を追加可能。
g 0 _             = 1
g x y | x > 0     = 1
      | otherwise = 2
-- 存在しないパターンの引数を渡されるとエラーになるため、それを引数のバリデーションに使うとよい。
main = print (h (-1))  -- エラー (なお、h -1と書くとh - 1 と解釈されて、hは数値でないからエラー)
h n | n > 0 = 1

-- Haskellにループの構文はないから、繰り返しは再帰で書く必要がある。
-- ( printN num - 1 str と書かないように注意!((printN num) - 1 でエラー) )
main = printN 4 "hello"  -- "hello hello hello hello"

printN :: Int -> String -> IO()
printN   1 str           = print str
printN num str | num > 1 = do print str; printN (num - 1) str

関数式 (ラムダ)

-- \引数1, 引数2, ... -> 式  (\はλをイメージしているらしい)
main = print $ add 3 5
add = \x y -> x + y  -- add x y = x + y と同じ

-- mapはリストの各要素に関数を適用する。(jsのArray#map的な。)
main = print $ map (\x -> x * x)[1, 2, 3] -- [1, 4, 0]

部分適用 (カリー化), 関数合成

-- 複数の引数をもつ関数は、左の引数から順に適用している。
-- したがって、左のいくつかの引数にだけ値を渡しておくことができる。
main = do
    print $ (add 3) 5 -- 8 を出力
    print $ f 5 -- 8 を出力
add x y = x + y
f = add 3

-- (2 +)や(+ 2)のように演算子の片方だけを適用すると残りのオペランドを受け取る関数になる。
main = print $ map (^ 2)[1, 2, 3]  -- [1, 4, 0]
-- なお、両方適用しないとふつうの関数になる。
main = print $ (^) 4 3 -- 64

-- -- f . gで関数f ∘ gを作る。以下の3つは同じ。
foo x = f (g (h x))
foo x = (f . g) x
foo = f . g

リスト操作

-- []で定義できるのは配列ではなくリストなので、ランダムアクセスは遅い。

-- 数学の{n | n ∈ N, n < 3}みたいな書き方でリストを作れる。(<-は∈の意味)
main = print [x | x <- [1..9], x < 5]  -- [1,2,3,4]
-- 全パターンの列挙
main = print [x + y | x <- [1..4], y <- [1..4]]  -- [2,3,4,5,3,4,5,6,4,5,6,7,5,6,7,8]

-- 連番リストの生成
main = print $ [1..4] -- [1, 2, 3, 4]

-- cons構成子(右結合) (:) :: a -> [a] -> [a] 
main = print (1 : 2 : 3 : [])  -- [1, 2, 3]
-- 要素の参照
main = print $ [1, 2, 3, 4] !! 2 -- 3
-- 連番リストは無限の長さを持てる。(print [1..]を実行すると無限ループする)
main = print $ [1..] !! 30  -- 31
-- リストの連結
main = print $ [1, 2, 3] ++ [4, 5]  --[1, 2, 3, 4, 5]
-- 文字列は[char]だからリストとして操作できる。
main = print $ "hello" !! 1  -- 'e'
-- その他の関数
length a {-リストの長さ-}, sum a {-全要素を加算した値-}, product a {-全要素を乗算した値-}, 
take n a {-先頭n個を抽出(take 2 [1,2,3] は [1, 2])-}, drop n a {-先頭n個を消したリストを返す-}, reverse a {-逆順のリストを返す-}


-- 関数定義で、引数にリストを受け取って先頭要素とそれ以降の要素に分ける構文がある。
listHead (x:_) = x  -- x='a',_="bcd"
main = print $ listHead "abcd"  -- 'a'

tail2 (a:b:c) = c  -- a='a',b='b',c="cdef"
main = print $ tail2 "abcdef"  -- "cdef"

タプル

main = do print (3, "hello")  -- (3,"hello")
-- 要素数が2つのときのみ、fst, sndで1, 2番目の要素を得られる
main = print $ snd (3, "hello")  -- "hello"
-- 要素数が3以上の時は、分割代入しないと要素を取り出せない (リストと違い!!は無い)
main = print el  -- 3
(_, _, el) = (1, 2, 3)

列挙型

-- dataで型を定義する。型名(Color), コンストラクタ(Blueなど)は大文字で始まる必要がある。
data Color = Blue | Red | Green  -- 列挙型
main = print Blue  -- エラー。デフォルトでは他の型へ変換できない。
-- derivingを使うと型変換などの機能を追加できる。
data Color = Blue | Red | Green deriving (Show, Enum)  --Showだけ指定するなら deriving Show
main = do print Blue; print $ fromEnum Green  -- Blue  2
deriving 機能
Show 文字列へ変換できる。show関数で文字列へ変換(main = print $ show 3 は "3"を出力)。
Enum 数値と相互変換できる。fromEnum関数で数値へ変換, toEnum関数で数値から列挙型へ変換。
Eq ==や/=で比較できる
Ord <や>で比較できる
Read 文字列から変換できる
Bounded 最小値と最大値をもつ

構造体(直積型)

data Point = Point Int Int deriving Show
addPoint (Point x1 y1) (Point x2 y2) = Point (x1 + x2) (y1 + y2)
main = print $ addPoint (Point 2 3) (Point 1 1)  -- Point 3 4

-- フィールド値の取り出しには分割代入が必須。
let (Point a b) = Point 3 4  -- a=3, b=4

フィールドに名前を付けられる。より他言語の構造体っぽくなる。

data Point = Point {x :: Int, y :: Int} deriving Show
main = print $ Point{x = 3, y = 4}  -- Point {x = 3, y = 4}
-- または
main = print $ Point 3 4

-- フィールドに名前がついていると、フィールド値の取り出しが簡単。
main = print $ y (Point 3 4)  -- 4  (フィールド名を関数として使ってフィールドの値を取得)

-- 特定のフィールドだけを変更する
a = MyStruct {field1 = 30, field2 = 100}
b = a {field1 = 50}  -- MyStruct {field1 = 50, field2 = 100}

フィールドが1つの場合はdataの代わりにnewtypeを使えて、動作は同じだがこちらの方が速い。

newtype MyInt = MyInt Int deriving Show
main = print $ MyInt 30  -- MyInt 30

直和型

列挙体と構造体を合体できる。これ、いつ使うのか? (追記: 型を再帰的に使うと木構造をすっきり書けるみたい。コメントを参照。)

data Foo = Bar Int Int | Baz Int Int Int

型別名

-- typeで型のエイリアスを作れる。
type String = [Char]

アクション

乱数や時刻取得などの、引数で返り値が一意に決まらない関数をアクションという。

import System.Random
-- 'A' ~ 'Z'のいずれかがランダムで出力される
-- (print =<< getStdRandom $ randomR ('A', 'Z') だと、=<<と$が干渉するせいか、エラーになる)
main = print =<< getStdRandom (randomR ('A', 'Z'))


-- アクションを実行して値を得るには、<-, =<<, >>=が必要になる。
-- アクションを代入
rand = getStdRandom $ randomR ('A', 'Z')
main = do 
  r <- rand  -- アクションを実行して値を変数に入れる (<-はdo{}内でしか使えない(実行順序が結果に影響するため、だと思う))
  print r
  -- または
  print =<< rand  -- アクションを実行して値を関数に渡す
  -- または
  rand >>= print  -- =<<を左右逆で書ける


-- mainはアクション。
main = rand  -- エラーにならない
-- printもアクション。
main = print =<< print 3  -- 3  ()

-- returnはアクションを生成するアクション。。
a = return 30  -- aは30を返すアクション
main = print =<< a  -- 30

add x y = return $ x + y  -- 関数addは引数x, yを受け取ると、x+yの値を返すアクションを生成して返す。
main = print =<< (add 3 5)  -- 8

-- do{}はアクションを順に実行する構文。do{}内でアクションではないことは実行できない。(a = 3 + 2 みたいな。)
-- do{}は最後の文の返り値を返す。早期returnは無い。
main = do
    print "abc"
    return 1    -- アクションを生成して捨てる (意味がない)
    print "def"
    return 2    -- 戻り値