59
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Haskell入門。関数からモナドまで

Posted at
1 / 76

このスライドはNakameguro.hs #1での発表に使用したHaskell入門のスライドです。

内容は全くの初心者からNoviceの人がAdvanced Beginnerになるための道筋が分かるように作ったつもりです。

説明だけでなく随所に演習も散りばめられているので手を動かしながら読んでいただけると幸いです。


Haskellの開発環境

コンパイラ: GHC
パッケージマネージャ: Cabal
依存管理: Stack

Stackを導入すればGHCとCabalもついてくるのでまずはStackを入れましょう。

Get Started

以下のaliasを設定しておくと便利です。

alias ghc='stack ghc --'
alias ghci='stack ghci --'
alias runhaskell='stack runhaskell --'

インストールしている間にこれから学んでいく概念を紹介。
Ladder of Functional Programming

NOVICE

  • 概念
    • イミュータブルなデータ
    • データの構築&分解
    • 第一級関数&ラムダ
    • 二階関数
    • 関数合成

ADVANCED BEGINNER

  • 概念
    • 代数的データ型
    • パターンマッチ
    • 一般的な再帰
    • パラメータ多相
    • 型クラス、インスタンス、法則
    • 低次の抽象化(Equal, Semigroup, Monoid 等)
    • 参照透過性、完全性
    • 高階関数
    • 部分適用、カリー化、ポイントフリースタイル

Haskell入門
~値と型と関数~


値と型

1, 2, 3 のような数字や"abc"のような文字列は値であり、それらはIntStringと型付けされる。1 + "abc"のような意味のない式が作れないように型は利用される。

$ ghci

> -- 値 :: 型

> 1 :: Int
1

> "abc" :: String
"abc"


> 1 + "abc"

<interactive>:3:1: error:
     No instance for (Num [Char]) arising from a use of +
     In the expression: 1 + "abc"
      In an equation for it: it = 1 + "abc"

ghciでは:tをつかって値の型を調べることができる。

> :t 1
1 :: Num t => t

> :t "abc"
"abc" :: [Char]

> :t (+)
(+) :: Num a => a -> a -> a

> :t ()
() :: ()

:iを使って型の情報を調べることもできる。

> :i String
type String = [Char]    -- Defined in ‘GHC.Base’


真偽値

> :i Bool
data Bool = False | True

GHCi

> True
True

> :t True
True :: Bool

> not True
False

> :t not
not :: Bool -> Bool

> 1 < 2
True

> True && False
False

> 1 < 2 && 2 < 3
True

> True || False
True

> if True then 1 else 2
1

ifは必ずthen, elseとの組で使用しどちらも省略することは出来ない。ifthenの間にはBool型の式を、thenelseの後には同じ型を持つ式を書く必要がある。以下はダメな例

> if True then 1

<interactive>:21:15: error:
    parse error (possibly incorrect indentation or mismatched brackets)

> if False then 1 else "abc"

<interactive>:22:15: error:
     No instance for (Num [Char]) arising from the literal 1
     In the expression: 1
      In the expression: if False then 1 else "abc"
      In an equation for it: it = if False then 1 else "abc"

数値型

説明
Int 整数
Integer 多倍長整数
Word 符号なし整数
Float 浮動小数点
Double 倍精度浮動小数点
Ratio 有理数
Complex 複素数

全てNumのインスタンス(型クラスに関しては後述)


数値型(GHCi)

> 1 :: Int
1

> 1 :: Integer
1

> (1 :: Int) + (1 :: Int)
2

> -- (+) :: Num t => t -> t -> t
> -- であるので2つの引き数の型は同じでなければならない
> (1 :: Int) + (1 :: Double)
<interactive> error:
   Couldn't match expected type Int with actual type Double

> 1 + 1
2

> -- 小数の例
> 0.2 / 0.3
0.6666666666666667

> -- 有理数の例
> import Data.Ratio

> -- モジュールをインポートする時はこのように
> -- import モジュール名
> -- とする

> 1%2
1 % 2

> 1%2 + 1%3
5 % 6

> -- 複素数の例
> import Data.Complex

> 1 :+ 2
1 :+ 2

> (1 :+ 2) * (1 :+ 3)
(-5.0) :+ 5.0

基本的な関数

> -- 加減乗除
> 1 + 1
2
> 2 - 1
1
> 2 * 3
6
> 4 / 2
2.0

> quot 4 2
2
> mod 5 3
2

> floor 1.3
1
> ceiling 1.3
2
> round 1.3
1

文字と文字列

説明
Char 文字
String 文字列

文字列の型Stringは文字の型Charのリストの型[Char]のエイリアスでしかない。

> :i String
type String = [Char]

文字と文字列(GHCi)

> -- 文字は ' で囲む
> 'a' :: Char

> -- 文字列は " で囲む
> "文字列" :: String

> -- 文字列の結合
> "abc" ++ "def"
"abcdef"

> -- 文字列の長さを求める
> length "abcdef"
6

-- 先頭の文字を取得する
> head "abcdef"
'a'

> -- 先頭3文字とってくる
> take 3 "abcdef"
"abc"

> -- 先頭2文字以降を取得する
> drop 2 "abcdef"
"cdef"

> -- 2文字目から4文字目を取得する
> ??? "abcdef"
"bcde"

> -- 3文字目を取得する
> ??? "abcdef"
'c'

> -- n文字目を取得する演算子
> "abcdef" !! 4
'e'

実はこれらの関数は全てリストに対して定義された関数!


> -- リストの長さを求める
> length [1,2,3,4,5,6]
6

-- 先頭の要素を取得する
> head [1,2,3,4,5,6]
1

> -- 先頭3要素とってくる
> take 3 [1,2,3,4,5,6]
[1,2,3]

> -- 先頭2要素以降を取得する
> drop 2 [1,2,3,4,5,6]
[3,4,5,6]

> -- 2つ目から4つ目を取得する
> take 4 (drop 1 [1,2,3,4,5,6])
[2,3,4,5]

> -- 3つ目を取得する
> head (drop 2 [1,2,3,4,5,6])
3

> -- nつ目を取得する演算子
> [1,2,3,4,5,6] !! 4
5

文字列に関する関数

> :t show
show :: Show a => a -> String

> show 1
"1"

> show True
"True"

> :t words
words :: String -> [String]

> words "this is a pen"
["this","is","a","pen"]

> :t unwords
unwords :: [String] -> String

> unwords ["this","is","a","pen"]
"this is a pen"

> :t lines
lines :: String -> [String]

> lines "aaa\nbbb"
["aaa","bbb"]

> :t unlines
unlines :: [String] -> String

> unlines ["aaa", "bbb"]
"aaa\nbbb\n"

リスト

> :i []
data [] a = [] | a : [a]
        ^
        '-- 型引数

リストは[]だけでは型ではなく別の型を与えて初めて型になる。
ex) [Int], [Char], [String], [[Int]]

この様に別の型を引数にとって型になる型を高階型という。


いろいろな高階型

説明
Maybe a 存在しないかもしれない値を表す型
[a] リスト
(a, b) タプル
Either a b aもしくはbを表す型
a -> b aの値を取ってbの値を返す関数の型
IO a 副作用を伴ってaの値を返す型

Maybe

> :i Maybe
data Maybe a = Nothing | Just a

存在しないかもしれない値を表す型

> import Data.List

> :t find
find :: (a -> Bool) -> [a] -> Maybe a

> find odd [2,4,6,8]
Nothing

> find odd [2,4,6,9]
Just 9

リスト(再び)

> :i []
data [] a = [] | a : [a]

リストの便利な記法

> [1..10]
[1,2,3,4,5,6,7,8,9,10]

> [1,3..20]
[1,3,5,7,9,11,13,15,17,19]

> [1..]
[1,2,3,4,5,6,7,8,9,10,11,12,13...

> ['a'..'z']
"abcdefghijklmnopqrstuvwxyz"

> -- リストの内包表記
> [x | x <- [1..10]]
[1,2,3,4,5,6,7,8,9,10]

> [(x, y) | x <- [1,2], y <- ['a', 'b']]
[(1,'a'),(1,'b'),(2,'a'),(2,'b')]

> [(x, y, z) | z <- [1..100], y <- [1..z], x <- [1..y], x^2 + y^2 == z^2]
[(3,4,5),(6,8,10),(5,12,13),(9,12,15),...

タプル

> :i (,)
data (,) a b = (,) a b

2つの値の組を表す型

> (1,2)
(1,2)

> -- 型が違っても大丈夫
> (1, "abc")
(1,"abc")

> :t fst
fst :: (a, b) -> a

> fst (1, "abc")
1

> :t snd
snd :: (a, b) -> b

> snd (1, "abc")
"abc"

3つ組(a,b,c)や4つ組(a,b,c,d)やそれ以上のタプルも存在する。


Either

> :i Either
data Either a b = Left a | Right b

二つのうちどちらか一方を表す型。Either String Intと型付けられた値はStringであるかIntであるかのどちらかである。

> -- 以下はどちらも同じ型

> Left "abc" :: Either String Int
Left "abc"

> Right 123 :: Either String Int
Right 123

Eitherは例外を扱いたい時に便利

> if 2 < 1 then Left "2は1より小さくないよ" else Right 100
Right 100

関数

> :i (->)
data (->) t1 t2

a -> baの値からbの値への対応付けの型。つまり関数。
\->を使ったラムダ記法という関数のリテラルが存在する。

> :t (\x -> not x)
(\x -> not x) :: Bool -> Bool

> (\x -> not x) True
False

Haskellの関数は何か特別なものではなく関数の型の付いた値でしかない。


関数と演算子

Haskellの関数には前置記法と中置記法がある

> -- 前置記法
> elem 1 [1,2,3]
True

> -- 中置記法
> 1 + 3
4

英字で定義されている関数は前置記法で、記号で定義されている関数(演算子)は中置記法で書かれる。バックチックや丸括弧を用いてこれらを入れ替えることもできる。

> 1 `elem` [1,2,3]
True

> (+) 1 3
4

また演算子に関してはセクションと呼ばれる部分適用の書き方が存在する

> (3 +) 2
5

> (+ 2) 3
5

例えばこんなことができる

> :t map
map :: (a -> b) -> [a] -> [b]

> map (+1) [1,2,3,4,5]

> :t filter
filter :: (a -> Bool) -> [a] -> [a]

> filter (3 <) [1,2,3,4,5]
[4,5]

高階関数

関数は値として扱えるので関数の返り値として関数を返すこともできる。
mapの型は

> :t map
map :: (a -> b) -> [a] -> [b]

であったが->は右結合なのでこれは

> :t map
map :: (a -> b) -> ([a] -> [b])

と解釈することができる。すなわちmapは関数を取って関数を返す関数である。このような関数を二階関数と呼ぶ。二階関数を返す関数を三階関数と呼び、より一般に関数を返す関数を高階関数と呼ぶ。


多変数関数

2変数関数を考えてみると以下の様な型になる

f :: (a, b) -> c

実はこの型は以下のような関数の型と同型である

g :: a -> b -> c

これは1変数の2階関数である。この様に2変数関数を1変数の2階関数に書き換えることをカリー化と呼ぶ。
同型であるとは互いに変換する関数が存在するということ。Haskellには上記のfgを同型たらしめる関数curry, uncurryがあらかじめ定義されている。

> :t curry
curry :: ((a, b) -> c) -> a -> b -> c

> :t uncurry
uncurry :: (a -> b -> c) -> (a, b) -> c

> :t take
take :: Int -> [a] -> [a]

> :t uncurry take
uncurry take :: (Int, [a]) -> [a]

一般に多変数関数は高階関数に変換することができる。またHaskellにおける関数は全て1変数関数である。


ファイルにHaskellのプログラムを書いてみる

$ vim Main.hs

Haskellのプログラムの拡張子は.hs

Main.hs
-- ↓ 関数の定義
add10 :: Int -> Int
add10 x = x + 10

-- ↓ 実行する関数
main :: IO ()
main = do
  print (add10 5)

関数の定義は型の宣言と処理の実装に分けられる。
型の宣言は必須ではないがトップレベルで関数を定義する時は必ず宣言するようにするのが良い。
main = doと書いたあとはインデントを下げて処理を書いていくのに注意。

$ runhaskell Main.hs
15

イミュータブル

Haskell の =代入ではない

x = 1

一度x1束縛するx は常に 1 を表す。
以下のようなプログラムはコンパイル時にエラーになる。
(実際に試してみて下さい)

x :: Int
x = 1
x = 2

main :: IO ()
main = do
  print x
$ runhaskell Main.hs
Main.hs:3:1: error:
    Multiple declarations of ‘x’
    Declared at: Main.hs:2:1
                 Main.hs:3:1

Haskellの変数は変更できないという意味で イミュータブル である。


関数の定義とラムダ

関数のリテラルとしてラムダ記法があった。これを使えば

f :: Int -> Int
f = \x -> x + 1

この様にfという変数に直接関数を束縛することができる。実は

f :: Int -> Int
f x = x + 1

という書き方はラムダ記法による定義の糖衣構文になっている。これを繰り返し用いれば

f = \x -> y -> x + y
f x = \y -> x + y
f x y = x + y

となるようにより高階な関数も糖衣構文を使って宣言できるようになる。


パターンマッチ

case..ofを使って値の構造を調べることができる。

isOneTwoThree :: String -> Bool
isOneTwoThree x = case x of
                      "one" -> True
                      "two" -> True
                      "three" -> True
                      _ -> False

isJust :: Maybe a -> Bool
isJust x = case x of
               Nothing -> False
               Just y  -> True

糖衣構文を使って以下のように書くこともできる

isOneTwoThree :: String -> Bool
isOneTwoThree "one" = True
isOneTwoThree "two" = True
isOneTwoThree "three" = True
isOneTwoThree _ = False

isJust :: Maybe a -> Bool
isJust Nothing = False
isJust (Just y) = True

例題

半径の値を受け取って円の面積を返すような関数を実装して実行してみて下さい。

> calcArea 2
12.566370614359172

> :t calcArea
calcArea :: Double -> Double

> -- 円周率は事前に定義されています
> pi
3.141592653589793

演算子の定義

演算子も関数と同じように定義できる。

(&) :: a -> (a -> b) -> b
(&) a f = f a

演算子の定義に使える文字はたくさんある。
スクリーンショット 2017-01-13 23.25.15.png

Haskell 2010 Language Report - Chapter 2 Lexical Structure

(≧∀≦*) :: String
(≧∀≦*) = "すごいHaskellたのしく学ぼう!"

演算子の結合性と優先順位を定義することもできる

-- 左結合で優先順位は1
infixl 1 &

Haskell 2010 Language Report - Precedences and fixities of prelude operators

ちなみに関数適用は優先順位が最も高い

> floor 1.2 + ceiling 1.5
3

$は右結合で優先順位が最も低いので括弧の代わりになる

> sum $ take 10 $ map (+1) [1,2,3,4,5]
20

関数合成の演算子(.)

> :t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c

例えば

> :t sum
sum :: Num a => [a] -> a

> :t take 10
take 10 :: [a] -> [a]

> :t map (+1)
map (+1) :: Num b => [b] -> [b]

> :t sum . take 10 . map (+1)
sum . take 10 . map (+1) :: Num c => [c] -> c

ここまでくれば
Noviceレベル!


休憩


Advanced Beginner
~Haskellの基本~


再帰

関数定義の中で自分自身を呼び出す。
例えば階乗を計算する関数を再帰的に定義してみる。

factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n-1)
factorial 3
= 3 * factorial 2
= 3 * (2 * factorial 1)
= 3 * (2 * (1 * factorial 0))
= 3 * (2 * (1 * 1))
= 6

次に与えられたリストの中身を全て掛け合わせる関数を再帰的に定義してみる。

prod :: [Int] -> Int
prod [] = 1
prod (x:xs) = x * prod xs
prod [1,2,3]
= 1 * prod [2, 3]
= 1 * (2 * prod [3])
= 1 * (2 * (3 * prod []))
= 1 * (2 * (3 * 1))
= 6

フィボナッチ数列のn項目の値を返す関数を実装してみて下さい。

1, 1, 2, 3, 5, 8, 13...


演習

H-99: Ninety-Nine Haskell Problemsより

以下のような関数を実装してみて下さい

Probrem1

> myLast [1,2,3,4]
4
> myLast ['x','y','z']
'z'

Probrem2

> myButLast [1,2,3,4]
3
> myButLast ['a'..'z']
'y'

Problem3

> elementAt [1,2,3] 2
2
> elementAt "haskell" 5
'e'

Problem4

> myLength [123, 456, 789]
3
> myLength "Hello, world!"
13

Problem5

> myReverse "A man, a plan, a canal, panama!"
"!amanap ,lanac a ,nalp a ,nam A"
> myReverse [1,2,3,4]
[4,3,2,1]

代数的データ型

独自の型を定義してみる。基本は

data 型名 = 値1 | 値2 | ...

例えば

data Signal = Green | Yellow | Red

cango :: Signal -> Bool
cango Green = True
cango Yellow = False
cango Red = False

各値には既存の型の値を持たせることもできる

> data Value = VInt Int | VStr String | VEith Int String

> :t VInt 5
VInt 5 :: Value

> :t VEith 1 "abc"
VEith 1 "abc" :: Value

レコード

data Person = Person { name :: String
                     , age :: Int
                     }

これは

data Person = Person String Int

name :: Person -> String
name (Person x _) = x

age :: Person -> Int
age (Person _ y) = y

を一度に定義してるのと同じ。

例題
Personのリストから20才以上の値だけ取り出す関数を実装してみて下さい

> :t adults
adults :: [Person] -> [Person]

> map name $ adults [Person "alice" 18, Person "bob" 20]
["bob"]

型クラス Show

自分で定義した方をghciで表示するためにはShowという型クラスのインスタンスにする必要がある。

data Signal = Green | Yellow | Red

instance Show Signal where
    show Green = "Green"
    show Yellow = "Yellow"
    show Red = "Red"

show :: Signal -> String を実装すれば良い。

instance Show Person where
    show (Person x y) = "{ name =" ++ x ++ ", age =" ++ show y ++ " }"
> Person "alice" 18
{ name = alice, age = 18 }

再帰的な型

自分でリストを定義してみる

data List a = Null | Cons a (List a)

instance Show a => Show (List a) where
    show Null = "Null"
    show (Cons x xs) = "Cons " ++ show x ++ " " ++ show xs
> Cons 1 (Cons 2 (Cons 3 Null))
Cons 1 Cons 2 Cons 3 Null

例題

Listに対してmyLastelementAtを実装してみて下さい

> myLast (Cons 1 (Cons 2 (Cons 3 Null)))
3

> elementAt (Cons 1 (Cons 2 (Cons 3 Null))) 2
2

再帰的な型2

data Tree a = Leaf a | Branch (Tree a) (Tree a)

例題

  1. Treeに対してShowのインスタンスを定義してみて下さい。
  2. 以下のようなLeafを数える関数myLengthを実装してみて下さい
> myLength (Branch (Leaf 1) (Branch (Branch (Leaf 2) (Leaf 3)) (Leaf 4)))
4
  1. 以下のような最大の深さを数える関数depthを実装してみて下さい
> depth (Branch (Leaf 1) (Branch (Branch (Leaf 2) (Leaf 3)) (Leaf 4)))
3

多相性

Haskellには多相性を実現する二種類の方法がある

  • パラメータ多相
  • アドホック多相

パラメータ多相
型変数を含んだまま関数を定義する。例えばリストの長さを返す関数 length はリスト中身の方を知らなくても実装することができる。

length :: [a] -> Int
length [] = 0
length (_:xs) = 1 + length xs

lengthaをどのような型に置き換えても同じ振る舞いをする。このような多相性をパラメータ多相と呼ぶ。

アドホック多相
引き数の型によって振る舞いが変わるような関数を定義する。


型クラス

アドホック多相を実現する仕組み。

class  Eq a  where
  (==) :: a -> a -> Bool
  x == y = not (x /= y)

  (/=) :: a -> a -> Bool
  x /= y = not (x == y)

instance Eq Bool where
  True  == True  = True
  False == False = True
  _     == _     = False

Eqのインスタンスを型ごとに定義することで(==)の振る舞いを型ごとに実装することができる。

elem :: Eq a => a -> [a] -> Bool

elemEqのインスタンスであるような型aに対して実装されている。

例題

data Person = Person { name :: String
                     , age :: Int
                     }

Personnameageが等しければ同じとみなすとしEqのインスタンスを定義してみて下さい。
また以下の式が実行できることを確かめて下さい。

> import Data.List

> elem (Person "alice" 18) [Person "bob" 20, Person "alice" 18]
True

型クラスの例

型クラス 型クラスが表す性質
Eq 等しいかどうかを判定できる
Ord 順序を定義できる
Bounded 最大値と最小値を持つ
Show 文字列に変換できる
Read 文字列から変換できる
Num 数値のように扱える

Eq

等しいかどうかを判定できる性質を表す型クラス

> :i Eq
class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool
  {-# MINIMAL (==) | (/=) #-}

例題

data List a = Null | Cons a (List a)

に対してすべての要素が等しい時に等しいリストとみなす、というようなEqのインスタンスを定義して下さい。

instance Eq a => Eq (List a) where
    ...
> Cons 1 (Cons 2 Null) == Cons 1 (Cons 2 Null)
True

> Cons 1 (Cons 2 Null) == Cons 1 (Cons 2 (Cons 4 Null))
False

Ord

順序を定義できる性質を表す型クラス

> :i Ord
class Eq a => Ord a where
  compare :: a -> a -> Ordering
  (<) :: a -> a -> Bool
  (<=) :: a -> a -> Bool
  (>) :: a -> a -> Bool
  (>=) :: a -> a -> Bool
  max :: a -> a -> a
  min :: a -> a -> a
  {-# MINIMAL compare | (<=) #-}

例題

data Signal = Green | Yellow | Red

に対して Green < Yellow < Red となるようなOrdのインスタンスを定義してみて下さい


Bounded

最大値と最小値を持つという性質を表す型クラス

> :i Bounded
class Bounded a where
  minBound :: a
  maxBound :: a
  {-# MINIMAL minBound, maxBound #-}
> maxBound :: Int
9223372036854775807

> minBound :: Int
-9223372036854775808

> maxBound :: Word
18446744073709551615

> minBound :: Char
'\NUL'

> minBound :: Bool
False

instance Bounded Signal where
    maxBound = Red
    minBound = Green

Show

文字列に変換できるという性質を表す型クラス

> :i Show
class Show a where
  showsPrec :: Int -> a -> ShowS
  show :: a -> String
  showList :: [a] -> ShowS
  {-# MINIMAL showsPrec | show #-}
> show 123
"123"

> show [1,2,3]
"[1,2,3]"

> show (Just "abc")
"Just \"abc\""

Read

文字列から変換できるという性質を表す型クラス

> :i Read
class Read a where
  readsPrec :: Int -> ReadS a
  readList :: ReadS [a]
  {-# MINIMAL readsPrec | readPrec #-}

> :t read
read :: Read a => String -> a
> read "1234" :: Int
1234

> read "1234" :: Char
*** Exception: Prelude.read: no parse

> read "Just 123" :: Maybe Int
Just 123

> read "[1,2,3,4]" :: [Int]
[1,2,3,4]

> read "123"
*** Exception: Prelude.read: no parse

Num

数値のように扱える性質を表す型クラス

> :i Num
class Num a where
  (+) :: a -> a -> a
  (-) :: a -> a -> a
  (*) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a
  {-# MINIMAL (+), (*), abs, signum, fromInteger, (negate | (-)) #-}

instance Num Word 
instance Num Integer
instance Num Int
instance Num Float
instance Num Double

deriving

型クラスの定義は面倒くさい?コンパイラに自動で定義させよう!

data Signal = Green | Yellow | Red deriving (Eq, Ord, Bounded)
> data Signal = Green | Yellow | Red deriving (Eq, Ord, Bounded, Show)

> Green == Yellow
False

> Green < Red
True

> maxBound :: Signal
Red

例題

Listに対してもEqShowのインスタンスが期待通りに導出されることを確かめてみましょう。


休憩2

休憩前に以下ののコマンドを実行しておいて下さい

$ stack install gloss http-conduit

IO入門
~副作用の扱い方~


参照等価性

ある式が参照透過であるとは、その式をその式の値に置き換えてもプログラムの振る舞いが変わらないことを言う。

wiki - 参照等価性

例えば1 + 1は勝手に2に置き換えても式の意味は変わらない。Haskellではどれだけ複雑な関数になってもこの性質が保たれる※。

逆に参照等価性を満たさない例

var count = 0;

function addCount(i) {
  count += i;
  return count;
}

console.log(addCount(1))
console.log(addCount(1))
console.log(addCount(1))

addCount(1)の値は評価するたびに変わってしまう。Haskellではこのようなことは起こらない。つまり 参照等価性を満たす関数は引き数が同じであれば常に同じ値を返す


副作用を起こす関数?

標準入力から値を受け取る関数は評価されるたびに値が変わるので参照等価性を満たさないのではないか?

main :: IO ()
main = do
  putStrLn "あなたの名前は何ですか?"
  name <- getLine
  putStrLn ("こんにちは" ++ name ++ "さん!")
-- 標準出力に文字列を出力する関数
putStrLn :: String -> IO ()

-- 標準入力から文字列を受け取る関数
getLine :: IO String

Haskellでは副作用を起こす処理をIOという型に包むことで参照等価性が破られないようにしている。詳しい話は難しいので割愛。


IOプログラミング

main = doと書いた下に行いたい処理を順番に書いていく。

main :: IO ()
main = do
    -- アクション1
    -- アクション2
    -- 結果 <- アクション3
    -- アクション4
    -- ...

アクションの例

type FilePath = String

-- FilePathにあるファイルの中身をStringとして返す
readFile :: FilePath -> IO String

-- FilePathにあるファイルの中にStringの値を書き込む
writeFile :: FilePath -> String -> IO ()

例題
テキストファイルを読み込んで各行の文字数を出力するプログラムを書いてみて下さい。


gloss

import Graphics.Gloss

main :: IO ()
main = do
    display (InWindow "Hello gloss." (640, 480) (100, 200)) orange Blank
$ runhaskell Main.hs
スクリーンショット 2017-01-14 3.12.08.png

Haddock を読んでみましょう: gloss


Haskellの文字列事情

StringはただのChar型のリストなのでメモリ的にも時間的にも効率が悪い。効率の良い文字列を表す型としてByteStringTextが存在する。

  • ByteString - 文字列をバイト列として扱う型
  • Text - ユニコード文字列を表す型

通常文字列リテラル"abc"Stringと型付けされるが、ファイルの先頭に

{-# LANGUAGE OverloadedStrings #-}

書くとByteStringTextとしても扱うことができるようになる。

{-# LANGUAGE OverloadedStrings #-}

import qualified Data.ByteString.Char8 as BS

main :: IO ()
main = do
  BS.putStrLn "This is bytestring."

実はIsStringのインスタンスになっている型なら文字列リテラルで表すことができる。


http-conduit

{-# LANGUAGE OverloadedStrings #-}
import Network.HTTP.Simple
import qualified Data.ByteString.Lazy.Char8 as BS

main :: IO ()
main = do
  response <- httpLBS "http://example.com"
  BS.putStrLn (getResponseBody response)
$ runhaskell Main.hs
<!doctype html>
<html>
<head>
    <title>Example Domain</title>

http-conduit


高次の抽象化


Functor

リストのmapの性質を型クラスを使って一般化する

map :: (a -> b) -> [a] -> [b]

a -> bという関数をaが別の型に包まれていても適用できるようにする

class Functor f where
  fmap :: (a -> b) -> f a -> f b

instance Functor [] where
  fmap = map

Functorとはa -> bf a -> f bという関数に対応付けることができるような性質を表す型クラス。


Functorのインスタンスたち

Maybe

instance Functor Maybe where
  fmap _ Nothing = Nothing
  fmap f (Just a) = Just (f a)
> fmap (+1) Nothing
Nothing

> fmap (+1) (Just 1)
Just 2

> fmap show (Just 123)
Just "123"

((,) a)

instance Functor ((,) a) where
    fmap f (a, b) = (a, f b)
> fmap (+1) ("a", 1)
("a",2)

> fmap show ("b", 123)
("b","123")

((->) r)

instance Functor ((->) r) where  
    fmap f g = f . g
> :t fmap (+1) (+2)
fmap (+1) (+2) :: Num b => b -> b

Applicative

関数適用の関数

($) :: (a -> b) -> a -> b

Functor の fmap

fmap :: Functor f
     => (a -> b) -> f a -> f b

Applicativeは関数と値がその型に包まれていても関数適用ができるような性質を表す

class Functor f => Applicative f where
    pure :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b

Applicativeのインスタンスたち

Maybe

instance Applicative Maybe where
    pure = Just
    Nothing <*> _ = Nothing
    (Just f) <*> k = fmap f k
> Just (*2) <*> Just 123
Just 246

> Nothing <*> Just 123
Nothing

> Just (*2) <*> Nothing
Nothing

> take <$> Just 3 <*> Just [1,2,3,4,5]
Just [1,2,3]

[]

instance Applicative [] where
    pure x = [x]
    fs <*> xs = [f x | f <- fs, x <- xs]
> [(+1), (*2)] <*> [1,2,3]
[2,3,4,2,4,6]

((->) r)

instance Applicative ((->) r) where
    pure = const
    f <*> k = \r -> (f r) (k r)
> :t take
take :: Int -> [a] -> [a]

> (take <*> (\i -> map (^i) [1..])) 1
[1]

> (take <*> (\i -> map (^i) [1..])) 2
[1,4]

> (take <*> (\i -> map (^i) [1..])) 3
[1,8,27]

> (take <*> (\i -> map (^i) [1..])) 4
[1,16,81,256]

Monad

($)   :: (a -> b) -> a -> b

fmap  :: Functor f
      => (a -> b) -> f a -> f b

(<*>) :: Applicative f
      => f (a -> b) -> f a -> f b

???   :: (a -> f b) -> f a -> f b

Functor であれば fa :: f ak :: a -> f b がある時

> :t fmap k fa
fmap k fa :: f (f b)

が作れる。しかし欲しかったのはf b

Monad はその型で二重に包まれている時に一重に潰すことができるような性質を表す

class Applicative m => Monad m where
    join :: m (m a) -> m a

この時

(>>=) :: Monad m => m a -> (a -> m b) -> m b
(>>=) fa k = join (fmap k fa) 

とすると、これが欲しかった???である。
逆に(>>=)を使って

join :: Monad m => m (m a) -> m a
join = (>>= id)

と定義することもできる。
実はHaskellではMonadは(>>=)を使って定義されている

class Applicative m => Monad m where
    (>>=) :: m a -> (a -> m b) -> m b

モナドはただの型クラスでした。


実は(>>=)pureがあればfmapが作れる

> :t pure
pure :: Applicative f => a -> f a

> :t (>>=)
(>>=) :: Monad m => m a -> (a -> m b) -> m b

> :t flip
flip :: (a -> b -> c) -> b -> a -> c

> :t flip (>>=)
flip (>>=) :: Monad m => (a -> m b) -> m a -> m b

> :t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c

> :t (pure .)
(pure .) :: Applicative f => (a -> a1) -> a -> f a1

> :t flip (>>=) . (pure .)
flip (>>=) . (pure .) :: Monad m => (a -> b) -> m a -> m b

fmap(>>=)があれば(<*>)が作れる

> :t ($)
($) :: (a -> b) -> a -> b

> :t flip ($)
flip ($) :: a -> (a -> c) -> c

> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f b

> :t fmap . flip ($)
fmap . flip ($) :: Functor f => a -> f (a -> b) -> f b

> :t flip (fmap . flip ($))
flip (fmap . flip ($)) :: Functor f => f (a -> b) -> a -> f b

> :t flip (>>=)
flip (>>=) :: Monad m => (a -> m b) -> m a -> m b

> :t flip (>>=) . flip (fmap . (flip ($)))
flip (>>=) . flip (fmap . (flip ($)))
  :: Monad m => m (a -> b) -> m a -> m b

例題
(<*>)pureを使ってfmapを作ってみて下さい

> :t (<*>)
(<*>) :: Applicative f => f (a -> b) -> f a -> f b

> :t pure
pure :: Applicative f => a -> f a

> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f b

実はFunctor, Applicative, Monadは好き勝手に実装する事はできず、以下の則を満たしていることが要請されている。

Functor則

fmap id == id

fmap f . fmap g = fmap (f . g)

Applicative則

pure id <*> v = v
pure f <*> pure x = pure (f x)
u <*> pure y = pure ($ y) <*> u

Monad則

pure x >>= f == f x
m >>= pure == m
(m >>= f) >>= g == m >>= (\x -> f x >>= g)

しかし、これら要請はコンパイラによって検知できるわけではないのでベストエフォートでしかない


Monadのインスタンスたち

Maybe

instance Monad Maybe where
    Nothing >>= _ = Nothing
    _ >>= Nothing = Nothing
    (Just f) >>= k = fmap f k
> :t elemIndex
elemIndex :: Eq a => a -> [a] -> Maybe Int

> Just 'a' >>= (\x -> elemIndex x "I have a pen")
Just 3

[]

instance Monad [] where
    xs >>= ks = concat [k x | k <- ks, x <- xs]
> import Data.Char

> :t \c -> [toUpper c, toLower c]
\c -> [toUpper c, toLower c] :: Char -> [Char]

> "abc" >>= (\c -> [toUpper c, toLower c])
"AaBbCc"

do構文

(>>=)は合成していくとすぐにぐちゃぐちゃになる

> elemIndex 2 [1,2,3] >>= (\x -> elemIndex 3 [3,4,5] >>= (\y -> if y == 0 then Just (-1) else Just (x + y)))
Just (-1)

実はdo(>>=)を使った合成の糖衣構文になっている。
上記の例はdo構文を使うと

do
  x <- elemIndex 2 [1,2,3]
  y <- elemIndex 3 [3,4,5]
  if y == 0 then Just (-1) else Just (x + y)

のように書ける。
糖衣構文を剥がすと

  elemIndex 2 [1,2,3] >>= \x ->
  elemIndex 3 [3,4,5] >>= \y ->
  if y == 0 then Just (-1) else Just (x + y)

となる。これを一行で書けば

elemIndex 2 [1,2,3] >>= (\x -> elemIndex 3 [3,4,5] >>= (\y -> if y == 0 then Just (-1) else Just (x + y)))

となり元の式と一致する


あらためてmain関数

main :: IO ()
main = do
  putStrLn "あなたの名前は何ですか?"
  name <- getLine
  putStrLn ("こんにちは" ++ name ++ "さん!")

IOは実はモナドなのでdo構文が使われている。
糖衣構文を剥がすと

main :: IO ()
main =
  putStrLn "あなたの名前は何ですか?" >>= \_ ->
  getLine >>= \name ->
  putStrLn ("こんにちは" ++ name ++ "さん!")

つまり

main :: IO ()
main = putStrLn "あなたの名前は何ですか?" >>= (\_ -> getLine >>= (\name -> putStrLn ("こんにちは" ++ name ++ "さん!")))

putStrLngetLineの型が

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

であったことを思い出すと、結果は確かにIO ()となっておりmain関数もただのHaskellの関数であったことがわかる。


おわり

59
51
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
59
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?