haskell-getting-started



haskell-getting-started

0 1


haskell-getting-started


On Github ggkuron / haskell-getting-started

Haskell 入門 vol.1 alpha

Haskell とは

好きなのどうぞ

  • C++ より速くて、Perl より簡潔で、Python よりきちんとしていて、Ruby より柔軟で、C# より型が充実していて、Java より頑強で、PHP とは何の共通点もないものって? に対する解答

  • 宇宙語

  • 非正格な評価を特徴とする純粋関数型プログラミング言語であり、高階関数や静的多相型付け、定義可能な演算子、例外処理といった多くの言語で採用されている現代的な機能に加え、パターンマッチングやカ

  • 楽しい

  • 型が全て

お断り

こんなん、一回でやり切れるはずなかったんや。。 主にスライドががが。。 ゆっくりしていきましょう。

いざとなればこれ使わせてもらいます

Haskell Platform

Haskellの全部入り開発環境パッケージ

とりあえず、これ入れよう。

  • プログラムを書いて実行

How to install haskell-platform

Ubuntu

sudo aptitude install haskell-platform

Fedora

sudo yum install haskell-platform

他のディストリビューションでも

How to install haskell-platform

Windows or Mac

http://hackage.haskell.org/platform/

OSXの場合、brewやportのhaskell-platformは古い可能性があるので上記推奨

エディタ問題

Haskell とは

遅延評価を特徴とする純粋関数型言語です。

ともかく使ってみよう

ghc

Glasgow Haskell Compiler

使い方としては3通り

  • ghc
    • ネイティブコードを生成するコンパイラ
  • ghci
    • 対話型インタプリタ
  • runghc
    • Haskellプログラムをスクリプトとして走らせるプログラム

対話インタプリタで遊ぶ

ghci を使う

シェルやコマンドプロンプトやPowerShellで

ghci

で起動する~はず~ ghciが起動すると最初にPrelude.hsという標準ライブラリが読み込まれる。

実際に書く

ghci で

> 1 + 2 * 3

と打ってみよう

7

が返ってくる 結合順も

1 + (2 * 3)

になってますね?

色々、試してみましょう

> True && True
True
> False || True
True
> not False
True
> 5 == 5
True
> 5 /= 5
False
> "hello" == "hello"
True

他の言語を触ったことのある人には違和感のない結果だと思います。≠が/=が変わってるくらいかな。

ghciでの変数定義

> let x = 10

これで x という名前に 10 が束縛された

> x -- Enter
10

これで x 自体が評価される

Haskellでの変数は一回定義した後は不変。

だけど、ghciでもう一度定義し直すと

> let x = 20
x -- Enter
20

上書きされている?

これは、前に定義されたxが隠されて新しいxが見えている。

この状態で、

 > x + 20 
 40

これは 20 + 20 と同等である

ファイルに記述して実行

hello.hs を用意して

double x = x + x

とでも書いておく

数値xを引数に取って、x+xを結果を返すdoubleという関数です。

ghci からファイルを読み込み

:l hello.hs

とすれば OK

> double 3 -- Enter
6

になったはず

コメント

-- から行末まではコメントですプログラムの実行には影響を与えません。

Haskellの特徴である純粋性について

純粋関数とは

副作用を持たない関数のこと。 ある関数を同じ引数で呼び出したら、同じ値が返ってくることが保証されるのが純粋関数。

副作用とは

関数は引数を適用してなにか返り値を得ることが目的。

  • その返り値のことを主作用。
  • それ以外の主作用以外の作用によって得られるもののことを副作用という。

副作用は複雑さをもたらすので、それを回避することはバグの低減に繋がる。

先ほど定義した関数doubleは純粋な関数。

-- 再掲
double x = x + x

何回double 3しても同じ結果が返ってきますね?

文字列をコンソールに出力する関数putStrLnだと

putStrLn "文字列"

これは副作用が目的の関数。

コンソールに文字を表示するように命令するけど、それが成功するかどうかはHaskellプログラムの関知するところじゃない。

Haskellは、この関数について情報を得られないので返り値は意味を持たない。

じゃあ、副作用が存在するじゃん。何が純粋関数型だ!という事ですが、この副作用を分離することで、純粋関数を有効に用いたプログラムが書ける。だから、Haskellは純粋関数型言語。~ということだと思ってる。~

Haskellの仕様には副作用についての記述はなく,それを切り離してるからHaskellは紛れもなく純粋関数型言語だ!! というご指摘いただきました。ありがとうございました。

正格評価と遅延評価

遅延評価を用いる関数型言語の標準となるべくHaskellは作られました。

関数を適用するときに引数をどのように評価するかのことを評価戦略といいます。

それの一種が遅延評価です。

hello.hsに

square x = x * x

という関数を追加で定義してみる。

ghciで

> :r

で前回読み込んだファイルを再読み込みできる。 もしくは、

> :l hello.hs

正格評価

引数を先に評価する戦略。多くの言語で使われている。

square (3 + 5)
square 8
8 * 8 
64

引数に渡された式が先に評価される。 最内簡約とも呼ばれる。

遅延評価

引数の評価をできるだけ後伸ばしにする評価戦略。

square (3 + 5)
(3 + 5) * (3 + 5)
8 * 8 
64

最外簡約

どちらにしても結果は変わらない。関数が停止可能なとき、遅延評価(名前呼び出し)では必ず停止するという性質がある。

式と値,そして型

Haskellでは、計算できる要素のことを式(expression)と呼ぶ。 Haskellではもうすべてが式、文(statement)~のようにみえるもの~でさえ式。

((1 + 2) * 3) + 4)
  • 1+2は式、
  • (1+2)*3は式、
  • そして全体 ((1 + 2) * 3) + 4)も式

  • さらに、+も、、式

その式の答え、計算の結果のことを値(value)と呼ぶ。 これ以上計算しようのない式のこと。

  • 数字の42
  • 文字'a'は値、

値であるなら式でもある

すべての式は何らかの型に結び付けられている。 だからもちろん全ての値はそれぞれの型をもつってわけ。

Haskellでの型の表記

42       :: Int
'a'      :: Char
"string" :: String
"a"      :: String
[1,2,3]  :: [Int]
(1,'a')  :: (Int,Char)

この表記方法のことを型シグネチャとか型注釈とか言います。

  • ::より左側は値の世界
  • ::より右側は型の世界

全くの別世界。同じ記号とか使ってても別物。

型の確認

ghciで:tに続けて式を書くと型を教えてくれる。

> :t 1 + 2
1+2 :: Num a => a

数値型であることが確認できる。この=>とかaはあとで説明します!!よろしくお願いします。

haskellの基本的な型

  • Haskellの型名は大文字から始まる。型の世界。
  • 変数名や関数名は小文字から始まる。値の世界。
    • 例外あり。

基本型page1

  • Int
    • CPUによって32bitとか64bitとか。
  • Integer
    • 半端無く大きい整数をつかうときにどぞ。
  • Float
    • 単精度不動小数点数
  • Double
    • 倍精度浮動小数点数

基本型page2

  • Char
    • Unicode文字を表す
    • 'a'
    • 'あ'
  • Bool
    • False
    • True のどちらかの値を持つ型
  • ()
    • 有効な値がないことを示す型
    • ()という値だけを持つ型
    • Unitと読む

Unit

有効な値がないことを示す型。 ()という値だけを持つ

() :: ()

値の世界と型の世界は別。 適切に定義された関数では、この型は副作用を持つ関数にしかあらわれない。

関数の型

すべての式が型を持つって言ったよね? じゃあ+の型は何なんだろう?

入力としてT1という型を取って出力としてT2という型を返す関数の型は、

T1 -> T2

と表現される。 だからIntを取ってIntを返す関数の型は

Int -> Int

+の型を確認してみてください。

> :t (+)

リスト

Haskellのリストは単一の型の要素だけを持てる。

  • 文字のリスト
  • Intのリスト
  • Floatのリスト

など。決して 混ぜられない。

[1, 2, 3, 4 ]  -- :: Num t => [t]
['a','b','c']  -- :: [Char]
[]             -- :: [a]
[1..4]

でも1から4までのリストを表現できる。

['a'..'z']

タプル

違う型でも一緒に括れるデータ構造、それがタプル。 括る数、括る型でそれぞれの型も異なったものとなる。

(1, "abc")  -- :: Num t => (t, [Char])
("abc", 1)  -- :: Num t => ([Cahr], t)
("abc",'a') -- :: ([Char], Char)
(1,2,3,4,5) -- :: (Num t, Num t1, Num t2, ..) => (t,t1,t2,t3,t4)
[1,2,3,4,5] -- :: Num t => [t]

これが全部、違う型

(1, "abc")  -- :: (数,文字列)
("abc", 1)  -- :: (文字列,数)
("abc",'a') -- :: (文字列,文字)
(1,2,3)     -- :: (数,数,数)
(1,2,3,4,5) -- :: (数,数,数,数,数)
[1,2,3,4,5] -- :: [数]

全部、違う型

リスト 関数

head [1,2,3,4] --  1
head "abc"     --  'a'

これでリストの先頭の 1 が取れる

tail [1,2,3,4] --  [2,3,4]
tail "abc"     --  "bc"

これでリストの先頭以外の残り [2,3,4] が取れる 尻尾というか本体みたいな感じある

last [1..4] --  4
init [1..4] --  [1,2,3]

(:) cons

1 : [2 3 4]   --  [1,2,3,4]
(:) 1 [2 3 4] --  [1,2,3,4]

1 つの要素とリストを連結して新しいリストを作る Haskellのcons

これは中置演算子として定義されている。 左辺が第1引数、右辺が第2引数となる2引数を取る関数。

+とか*とかと同じですね。

横道

Haskellでは、 中置演算子として定義されている関数は、括弧()で囲むことで通常の関数として使える。

1 + 2 -- 3
(+) 1 2 -- 3
((+) 1 2) -- 3
((+) ((*) 3 5) 2) -- 17

逆に、2引数を取る関数があると、それをバッククォートで囲むことで中置演算子として使える。

f x y = x + y
f 1 2   -- 3
1 `f` 2 -- 3

リストの正体

[1,2,3,4]

というリストは、実は

1 :(2 :(3 :(4 :[])))

という形になっている

リスト連結

[1,2,3] ++ [3,4,5]
[1,2,3,3,4,5]

リストの連結は(++)で行える。 が、この操作は前方のリストを最初から最後まで走査するコストがかかるので 使えるかぎり(:)を使うといい。

関数定義

f :: Int -> Int -- 型の世界(型注釈)
f x  = x + 1    -- 値の世界

これで引数 x に 1 を足す関数 f が定義された。Intを取ってIntを返す関数という型注釈をつけてみた。

f 2 -- > 3
f 2.0 -- > 型エラー

これはIntを取ってIntを返す関数として定義されたので、 浮動小数点数は受け付けない。 型で弾く(安心)

型注釈をつけなくてもコンパイラは賢いので推論してくれる。だけど、人間はコンパイラほど賢くないのでコンパイラに意図を伝えておくために 極力型注釈は付けるようにすべき。

型クラス

Num a => a

とか

Fractional a => a

とかの表記についてです。

型クラスとは、振る舞いを定義するインタフェース。ある型クラスのインスタンスであるということは、要求される関数を定義している型であるということ。

型変数

ここでのaは実際の何らかの型を表す変数、型変数。 多相的に型を扱うために使われる。

リストは単一の型だけを保持できるデータ構造でした。リストは保持している型によってそれぞれの型が違う。

[1,2,3,4,5]   :: Num t => [t]
['a','b','c'] :: [Char]
[1.0,2.0,3.0] :: Fractional t => [t]

例えば、headはリスト先頭の要素を返す関数だったけど、関数は式で、式は型に結び付けられている。1つのheadという関数で全てのリストに対する定義するためには型変数がいい働きをしてくれる。

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

これはどんな型でもいいからaという型を包んだリストを取ってその包まれたaという型を返す関数

型変数としてはaとかbとかcとかがよく使われる。

もうひとつリスト連結関数(++)を見ておきましょう。

[1..5] ++ ['a','b','c'] -- だめ。
> :t (++)
(++) :: [a] -> [a] -> [a]

aは実際の何らかの型を表す。

  • 第1引数のaはNum t => t
  • 第2引数のaはChar

になって、型があわない。

Num a => a
Fractional a => a

=>の左側を型制約と呼ぶ。

:t 30 :: Num a => a

型変数aはNum型クラスのインスタンスである、という制約を加えている。インスタンスになるためにはそのクラスが既定している関数を実装する必要がある。この制約によって、Num型クラスが定義を要求する関数をaが備えた型だということをコンパイラに伝えられる。

Num 型クラス

Numは数の型クラス

これのインスタンスになるためには以下を要求する

  • (+) :: a -> a -> a
  • (*) :: a -> a -> a
  • (-) :: a -> a -> a
  • negate :: a -> a
  • abs :: a -> a
  • Int
  • Integer
  • Float
  • Double などがこのインスタンス

今まで見てきたように数字リテラル1とかは多相定数としてこのNumクラスの任意のインスタンスとして振る舞う

:t 20
20 :: Num t => t

Fractional 型クラス

Fractionalは分数の型クラス

  • /
  • recip

を要求する Float,Doubleがこのインスタンス

Eq 型クラス

等値比較のための型クラス Haskellの基本型は全てこの型クラスのインスタンス。

  • ==
  • /=

各型クラスの要件はghciで

:i Num

のようにして知ることができる。

無名関数(λ関数)

λ式を使って無名関数を作れる。

\x -> x + 1

名前の無い関数。目を細めれば\はλに みえてきた…?

どうやって呼び出す

もしもその関数に f という名前が付いていれば

f 1

で良い

ならば

(\x -> x + 1) 1 -- > 2

で良い

条件分岐

if True then 1 else  2 -- > 1

elseは必須

if False then 3 else 4 -- > 4
if 1 == 2 then 10 else 20 -- > 20

完全な余談

if-then-elseは遅延評価のHaskellでは関数で問題なく定義できる。 だけどHaskellのifはcase式へのdesugarになってる。 理由は

パターンマッチ

関数の本体を引数の形で場合分けできる。 数値、文字、リスト、タプル、Boolなど多くの型で使える。

isThree :: Int -> Bool
isThree 3 = True
isThree _ = False
factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)
head' x:xs = x
head' []   = []

どれにも合わないパターンがあると例外が飛ぶ 。 オプションつければghcが叱ってくれる。

ガード

引数の形が同一でもさらに条件を絞って場合分けできる。

max' :: (Ord a) => a -> a -> a
max' a b  
    | a <= b    = b
    | otherwise = a

=の左側がBool型の値となる式です。 otherwiseってTrueです。 上側から順に合致するまで下っていきます。 合致したところのみ関数本体が評価されます。

空リストに配列操作関数を使うと

head [] -- > Exception! 
tail [] -- > Exception!
init [] -- > Exception!
last [] -- > Exception!

全域関数にはなっていない。 では、これを全域関数で定義し直しましょう。

型のことがわかったので、同一の型となるようにそれぞれ定義してみましょう。

tail' x:xs = xs
tail' [] = []

これはパターンマッチの練習に。

リストの直接 n 番目の要素が欲しい

こういう関数が欲しい。ていうかあったけど忘れた。 …というときの探し方。 hoogleにアクセス。 欲しい関数の型は

[a] -> Int -> a

に類するような型

まさにこれな(!!) という関数がある これは中置演算子とし定義されているので

[10,20,30,40] !! 2 -- > 30

これでOK!

リスト操作の種々の関数

map

全要素に特定の関数を適用した結果のリストを得る

map (\x -> x * 2) [1,2,3] -- > [2,4,6]

foldr

全要素を特定の関数で右から畳み込む

foldr :: (a -> b -> b) -> b -> [a] -> b
foldr (+) 0 [1,2,3] -- > 6

もともと渡したリストは

[1,2,3]
(1:(2:(3:([]))))

この:を引数として渡した2引数を取る関数に[]を引数として渡した初期値に置き換える

(1+(2+(3+(0))))

これがfoldr

foldl

foldl (+) 0 [1,2,3] -- > 6
((((0)+1)+2)+3)

これが左畳み込みfoldrを使うべきかfoldlを使うべきかは処理の効率を考えて

これは、また今度

foldr

foldr (\x y -> x * 10 + y) 0 [1,2,3]

これは

x # y = x * 10 + y

(1:(2:(3:([]))))

に対して適用させることなので、

(1 #(2 #(3 #(0))))

と等価なので 60 となる

foldl (#) 0 [1,2,3]

だと

(((0 # 1)# 2)# 3)

と等価なので 123になる。

無限リスト

遅延評価の仕組みのおかげで無限長のリストを扱える

repeat 1 -- > [1,1,1,1,1,1,1 ...)

このままでは ghciが永遠出力し続ける (Ctrl + C で中断可能)

repeat

与えられた引数を無限に要素に持つリストを返す

repeat 10 -- > [10,10,10, ...]

cycle

与えられた引数のリストを繰り返す無限長リストを返す

cycle [1,2,3] -- > [1,2,3,1,2,3,1,2,3 ...]

iterate

与えられた関数を繰り返し適用して得られる要素が並ぶリストを返す

iterate (/x -> x * 10) 1 -- > [1,10,100,1000,10000 ...]

iterate (/x -> + x 1) 0 -- > [0,1,2,3,4 ...]

無限リストから有限の要素を取り出す

take

take 5 $ repeat 1 -- > [1,1,1,1,1]

take 10 $ cycle [3 2 1] -- > [3,2,1,3,2,1,3,2,1,3]

もちろん take は有限長のリストにも使える $は開き括弧と無限遠方にある閉じ括弧のペアだと思うといい。

どのように定義されているかを見たければhoogleへ!

take 5 $ [1,2,3,4,5,6,7] -- > [1,2,3,4,5]

無限リストを畳み込む

and' :: [Bool] -> Bool
and' xs = foldr (&&) True xs

例えば

[True,True,False,True]

を与えてみると,このリストは表記を変えると

True :(True:(False:(True:([]))))

なので定石通り,foldrを適用して

True (&&)(True (&&)(False(&&)(True(&&)(True))))
True (&&)(True (&&)(False(&&)(True && True)))
True (&&)(True (&&)(False && True))
True (&&)(True && False)
True && False
False

となる

これを無限リストに適応すると

and' $ repeat False
-- and' $ [False, False, False ...]
-- False (&&)(False (&&)(False ...
False

&&は引数1つ目がFalseなら後ろを評価せず停止するように定義されているんです。

(&&) :: Bool -> Bool -> Bool
True && True = True
_    && _    = False

遅延評価だからそこで停止するんです。 正格評価だと引数の評価をすべて先にしにいくので止まりません。

zipWith

関数を使って2つのリストを1つにする

zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith (+) [1..9] [9,8..1]

filter

全要素を特定の関数でフィルタリングして残ったもののみで構成されたリストを得る

:t filter
filter :: (a -> Bool) -> [a] -> [a]
filter (\x -> x >= 5) [1..9] -- > [5,6,7,8,9]

文字列

"abc" -- :: [Char]
""    -- :: [Char]
[]    -- :: [a]
> :t "abc" 
"abc" :: [Char]
> :t 'a'
'a' :: Char
"abc" ++ "def" -- > "abcdef"

Int からCharを作る関数が欲しいなら その型はInt -> Charなので hoogleで調べる。 Char -> Intもついでに。

intToDigit 1 -- > '1'

Hello World!

putStrLn "Hello World!"

println は文字列を行として標準出力に表示する関数

:t putStrLn
putStrLn :: String -> IO ()

IO ()

Unitの結果をもたらすAction型

IO

モナドと呼ばれる型クラスのインスタンス。 モナド。恐ろしい名前

しかし、ただの型クラスだ。

連想リスト

Data.Mapに定義されている 一般的には連想リストと呼ばれてるものだけど、Haskellのリストは使われてないので、 Mapと呼ぶこと。 自前でリストで定義するより高速になるように作られている。 PreludeやData.Listと競合する名前が使われているので修飾インポートする

import qualified Data.Map as Map
number = Map.fromList [(1,"one"), (2,"two"), (3, "three")]
Map.lookup number 1 -- "one"

上記の例は 1 というキーの値が "one",2 というキーの値が "two" のマップである.

今回は説明できなかったこと

  • Monad

プロジェクト作成

ディレクトリを作って

mkdir appdir

cabalファイルを作って

vim app.cabal
Name:   MyApp
Version: 0.0
Description: 
Build-Type: Simple
Cabal-Version:  >=1.2
Executable MyAppExecutable
  Main-is:    main.hs
  Build-Depends: base >= 2.0
               , free-game >= 1.0

実行可能ファイル作成

cabal setup
cabal build

main という関数が main 関数 (Moduleを記述するとMainというモジュールのmain関数を探しに行く) ここから起動する

引数を受け取ってみる

module Main where
import System.Environment (getArgs)

main :: IO ()
main = do
    args <- getArgs
    putStrLn ("Hello, " ++ args !! 0)

一番上の二行

  • Mainという名前のモジュールをこれから書く
  • それは、Systemモジュールの一部をimportする。

Haskellの全てのプログラムは Mainモジュールのmainアクションから実行開始する。

module名は一文字目大文字 定義名は一文字目小文字

IO ()

ユニットの結果をもたらすAction型

IO 型

モナドと呼ばれる型クラスのインスタンス。 モナド。恐ろしい名前 次のように改造

実行してみる

./MyAppExecutable
Hello, World!

ビルド

cabal build

引数を与えて実行

./a.out World!
Hello, World!

cabal

Common Build And Tool

Haskell用のビルド、パッケージングシステム。 Haskellのpackage manager gemとかに類するもの 欲しいパッケージがあればこれを使って簡単に導入できる。

参考書籍 1

すごいHaskellたのしく学ぼう

参考書籍 2

プログラミングHaskell

参考書籍 3

実践F# 関数型プログラミング入門

参考資料

The Haskell School of Music

勉強に使えるサイト