メインコンテンツへ移動

基礎構文_Haskell ノート 1

無料2018-04-21#Functional_Programming#Haskell入门指南#纯函数式语言#函数式思维#Haskell教程#Haskell tutorial

この非常に純粋な関数型言語を再び手に取り、好きになり始める

一.簡介

Haskell は純粋関数型言語(purely functional programming language)であり、その関数型特性の純粋性に異論はありません

命令型言語は求解のステップを提供することを要求するが、Haskell は問題の記述を提供することを傾向とする

  • 非関数型思考:コマンドを通じてコンピュータに何をするかを指示する。例えば総和はループ構造を通じてすべての数値を走査し、加算してその和を記録する

  • 関数型思考:関数を通じて問題が何かを記述する。例えば総和は最初の数値と残りの数値の和を加算する

P.S. 思考モードの差異については、一場関数型思考モードの洗礼 を参照

Haskell の特徴:

  • 変数不変:関数型の変数と定数の概念は同じ。数学的思考に由来。x=1 とすれば、x は永遠に 1

  • 参照透明:関数呼び出しは対応する値に直接置換可能で、関数の動作に影響しない。つまり関数は値を求めるためにのみ使用され、副作用なし(外部状態に影響しない)。同じ入力は常に同じ出力を得る

  • 遅延評価:本当に値が必要な時に初めて計算する。そのため此时の一連の計算(関数呼び出し)は入力データに対する一連の変換公式に過ぎない。具体的には array.map().filter().reduce()array を 1 回だけ走査し、3 回ではない

  • 静的型:コンパイラは静的型チェックを行う。これは特に奇妙ではないが、強力な自動型推論もサポートする。そのため多くの場合型を宣言する必要がない。これにより静的型チェックの利点を持ちながら、コードの簡潔さも保証

P.S. 参照透明(Referential transparency)の詳細説明:

An expression is said to be referentially transparent if it can be replaced with its corresponding value without changing the program's behavior. As a result, evaluating a referentially transparent function gives the same value for same arguments. Such functions are called pure functions. An expression that is not referentially transparent is called referentially opaque.

二.基本演算

負数と単項マイナス

-3

数字 3 に単項演算子 - を使用し、その相反数 -3 を求める。つまり、-3- は数値リテラルの一部ではなく、演算子

2 + -3

エラーになる:

cannot mix '+' [infixl 6] and prefix `-' [infixl 6] in the same infix expression

二項演算子と単項演算子を同じ中綴式で混在できない。解析時の不確実性(曖昧さ、コンパイラがどのように理解すべきか分からない)をもたらす。そのため、経験原則としてすべての負数リテラルに括弧を付ける。例えば (-3)

P.S. Haskell には単項演算子が 1 つしかない。単項マイナス -。詳細は Unary operator を参照

論理演算

3 つの演算子:与(&&)、或(||)、非(not

2 つの Bool リテラル:TrueFalse

等価性判断

  • ==:等しいを判断。文字列に使用可能

  • /=:等しくないを判断(数学者の言語。そのため不等号はこのように見える

注意:型は厳密に一致しないと比較できない。否则エラーで比較可能性がないと見なされる(1 == True はエラー)。しかし整数と浮動小数点数は比較可能と見なされる(1 == 1.0True

演算子優先度

GHCi 環境では info: コマンドで演算子優先度を確認可能。例えば:

> :i *
class Num a where
  ...
  (*) :: a -> a -> a
  ...
    -- Defined in 'GHC.Num'
infixl 7 *

> :i -
class Num a where
  ...
  (-) :: a -> a -> a
  ...
    -- Defined in 'GHC.Num'
infixl 6 -

乗算は減算より優先度が高い(それぞれ 76)。どちらも中綴関数(infixlinfix)。どちらも左結合(infixllleft associative を表す)。関数シグネチャも同じ(Num a => a -> a -> a

優先度の範囲は 0-9。値が大きいほど優先

P.S. infixl 7 * は fixity 宣言と呼ばれ、演算子(関数)の結合性と優先度を指定。例えば:

infixr 5 <+
x <+ xs = x : xs
infixl 5 +>
x +> xs = xs ++ [x]

2 つのカスタム中綴演算子 <++> を宣言。それぞれ右結合と左結合。優先度はどちらも 5(: と同じ)

三.関数呼び出し

構文形式

Haskell の関数呼び出しはデフォルトで接頭辞構文。例えば:

succ 2
min 1 (-2)

Bash スクリプトの関数呼び出し構文と同じ。関数名 パラメータ 1 パラメータ 2

しかし演算子は特殊な関数として、デフォルトで中綴形式で呼び出す。例えば:

1 + 2

実際、任意の関数(演算子を含む)は接頭辞または中綴形式で呼び出し可能。ルールは以下の通り:

--  接頭辞を中綴に変換
prefixFunc a b
a `prefixFunc` b

--  中綴を接頭辞に変換
a infixFunc b
(infixFunc) a b

例えば:

1 `min` (-2)
(+) 1 2

dollar 関数

$ も関数:

($)
forall (r :: GHC.Types.RuntimeRep) a (b :: TYPE r).
(a -> b) -> a -> b
    -- Defined in 'GHC.Base'
infixr 0 $

優先度が最低の中綴右結合関数。シグネチャから見ると、単なる関数呼び出し記号。右側に括弧を追加することに相当:

-- 自然数の平方和を求めて第何個で 1000 を超えるか
length (takeWhile (< 1000) (scanl (+) 0 (map sqrt [1..])))
-- 等价于
length $ takeWhile (< 1000) $ scanl (+) 0 $ map sqrt [1..]

優先度が最低で、演算に影響せず、演算順序のみ調整:

> max 5 3 * 2 + 1
11
> max 5 $ 3 * 2 + 1
7

単純に $ を括弧の代替品と理解するのは不適切。例えば:

> 3 * $ 5 - 2 + 1

<interactive>:21:5: error:
    parse error on input '$'
    Perhaps you intended to use TemplateHaskell

姿勢を変更:

> (3 *) $ 5 - 2 + 1
12

$ は中綴関数で、左側は関数、右側はそのパラメータである必要がある

P.S. もう 1 つ面白いものがある:($ 2) sqrt。中綴関数カリー化の小技

カリー化

Haskell関数はデフォルトで全てカリー化され、1 つのパラメータのみを受け取る

In Haskell, all functions are considered curried: That is, all functions in Haskell take just one argument.

例えば:

> :t (/)
(/) :: Fractional a => a -> a -> a
> :t (/ 2)
(/ 2) :: Fractional a => a -> a

その中で、(/ 2)(/) :: Fractional a => a -> a -> a 関数の不全调用(partially applied)。そのため計算結果を得られず、関数 (/ 2) :: Fractional a => a -> a を返す

P.S. (-)関数は特殊。(- 2) は負 2 を表し、新しい関数を返さない。強いて言えば (subtract 2) を使用

curryuncurry 関数を通じてカリー化または非カリー化可能:

-- uncurry 非カリー化
> :t uncurry (/)
uncurry (/) :: Fractional c => (c, c) -> c
> (uncurry (/)) (4, 2)
2.0

-- curry カリー化
> :t curry (uncurry (/))
curry (uncurry (/)) :: Fractional c => c -> c -> c
> (curry (uncurry (/))) 4 2
2.0

注意:多パラメータ関数はタプル形式でパラメータを渡す。例えば f (4, 2)

カリー化特性を利用する時はパラメータ順序に注意。例えば:

> (/ 2) 4
2.0
> (2 /) 4
0.5

偏関数適用

偏関数適用(partial application)とカリー化(currying)の最大違いはパラメータ数量への影響。関数呼び出し求値の角度から見ると、カリー化はパラメータ数量を変更せず、偏関数適用はパラメータ数量を減少させる。いくつか予め埋めるため。例えば:

fn (a, b) = a + b
curriedFn = curry fn
partialFn = curriedFn 2

// 関数呼び出し求値
fn (2, 3)
--  括弧を追加して結合性をより明確に
(curriedFn 2) 3
partialFn 3

したがって、両者の联系は、カリー化関数を通じて偏関数を作成可能(partialFn = curriedFn 2)。違いは目的が異なること。偏関数適用は関数が必要とするパラメータ数量を減少させるため(いくつかのパラメータ値を固定)。カリー化は多パラメータ関数を単パラメータ関数に変換するため。この単パラメータ関数は別の単パラメータ関数を返す(パラメータ数量不足)、または求値(パラメータ数量十分)

四.関数宣言

doubledouble x = double (double x)

double x = x * 2
isEven x = x - x `div` 2 * 2 == 0
x `mod'` y = x - (x `div` y) * y

形式は関数呼び出しとほぼ同じ。関数名にスペースで区切られたパラメータリスト。= の後ろは関数本体

2 つの特徴:

  • 宣言順序は无所谓

  • 関数名の先頭文字は大文字不可。数字で開始不可

P.S. 数学では類似のものを x x' x'' の命名習慣で表す。Haskell でもこのように可能:

y x = x ^ 2
y' x = x ^ 2 + 1

また、中綴形式変換は関数宣言でも使用可能:

x `mod'` y = x - (x `div` y) * y

一部のシーンで関数宣言の可読性を向上可能

無パラメータ関数

定数は無パラメータ関数と理解可能。例えば:

> :t 2
2 :: Num t => t

またはより生きた例:

-- 無パラメータ関数、つまり const
two = 1 + 1

匿名関数

匿名関数は関数式。Haskell では lambda と呼ぶ。構文形式は以下の通り:

バックスラッシュ + パラメータリスト -> 関数本体

例えば:

sum' = \x y -> x + y

P.S. JS の const sum = (x, y) => x + y に類似

应用场景から見ると、lambda の特徴は:

  • 使い捨て:再利用を要求しない

  • 十分に簡単:自己説明。関数本体から一目で機能が理解できる

例えば:

map (\x -> x + 1) [1, 2, 3]
map (\([x, y]) -> x + y) [[1, 1], [2, 2], [3, 3]]

しかし多くの場合、明示的に lambda 構文を通じて関数を作成する必要はない。カリー化、List Comprehension などの特性を十分に利用可能:

map (+1) [1, 2, 3]
[ x + y | [x, y] <- [[1, 1], [2, 2], [3, 3]]]

また、面白いものがある:

addThree = \x -> \y -> \z -> x + y + z
-- haskell は自带 currying のため、等价于
-- addThree x y z = x + y + z

P.S. 匿名関数の -> と型宣言の -> は意味が同じ。どちらも「マップ先」(maps to)を表す

関数合成

数学の関数合成の表現方式は f·g(x) = f(g(x))。Haskell もこれに類似:

fg = f . g

使用する演算子は .

(.) :: (b -> c) -> (a -> b) -> a -> c 	-- Defined in 'GHC.Base'
infixr 9 .

右結合。そのため f . g . h xf (g (h x)) と等价:

((/7) . (*2) . (+3)) 4

関数合成は新しい関数を作成可能。またパラメータを引き抜くことも可能。例えば:

-- f x = 2 * (sqrt x) + 1
-- 対応する pointfree style
f = (+ 1) . (* 2) . sqrt

合成を通じて内層パラメータを引き抜き、カリー化特性を利用して除去。この関数合成のみを通じて得られ、実際のパラメータを涉及しない関数スタイルは pointfree style と呼ばれる

P.S. 注意。極端に長い関数チェーンは可読性を低下させる。このようにすることは推奨されないlet/where などの宣言を通じて関数チェーンを分割し、意味を付与すべき

五.条件文とパターンマッチ

条件文

-- if then else
gt3 x = if x > 3
  then True
  else False

注意:if-then-else 完全構造。else 部分省略不可

面白いのは、if 文も式であるため、このように可能:

lt10 x = if x < 10 then True else False
gte10 x = not (if x < 10 then True else False)

パターンマッチ

パターンマッチは基本的な関数呼び出しメカニズム。例えば:

sayOneTwoThree :: (Integral a) => a -> String
sayOneTwoThree x = "Not between 1 and 3"
sayOneTwoThree 1 = "One!"
sayOneTwoThree 2 = "Two!"
sayOneTwoThree 3 = "Three!"

関数を呼び出す時は宣言順序に従ってパラメータ型をマッチ。そのため上記の sayOneTwoThree 2"Not between 1 and 3" のみを返す

またパターンマッチを利用して再帰で階乗を求める例:

fact 0 = 1
fact n = n * fact (n - 1)

注意。パターンマッチに失敗するとエラーになる:

mod10 0 = 0
mod10 1 = 1
-- 最後に万能マッチでカバーしないと、mod10 2 はエラーになる
-- mod10 x = x `mod` 10

マッチ失敗時:

> mod10 2
*** Exception: t.hs:(27,1)-(28,11): Non-exhaustive patterns in function mod10

mod10 関数のパターン定義に漏れがあると提示

関数パラメータ定義に使用する他に、パターンマッチは List Comprehension や let-in 式、where 句などのシーンでも使用可能。例えば:

[ x + y | (x, y) <- [(1, 2), (3, 4)] ]
sumOneTwoThree = let (a, b, c) = (1, 2, 3) in a + b + c

一般的なパターンマッチテクニックは以下の通り:

--  list の先頭要素と残りを分割。length >= 1 が必要。否则エラー
x:xs
--  list の最初の 2 つの要素と残りを分割
x:y:ys
--  分割と同時に元参照を保持
xs @(x:y:ys)

P.S. @ で元参照を保持するのは as パターンと呼ばれる

Guard

シンプルな guard パターン例:

plus'' a b
  | a > 0, b > 0 = "sum is a positive value"
  | a < 0, b < 0 = "sum is a negative value"
  | otherwise = "what?"

パラメータリストの後ろに | 条件 が追加され、異なる関数本体ブランチを表す。呼び出された時に条件を満たせば対応する関数本体を実行して返す。否则順序に従って順に下にチェック

注意。最後の otherwise は面白い。なぜなら:

> :i otherwise
otherwise :: Bool 	-- Defined in 'GHC.Base'
> otherwise == True
True

したがって otherwise は意味的需要のみ。直接 True をデフォルトブランチの条件としても可能

P.S. 単行形式も合法(しかし可読性が悪く、使用を推奨しない)。例えば:

isPositive n | n > 0 = True | otherwise = False

where キーワード

where キーワードは guard の後に続き、関数宣言で必要な変数と補助関数を定義するために使用

checkArea r
  | area < little = addSpace "little circle" ++ wrap sArea
  | area < normal = addSpace "normal circle" ++ wrap sArea
  | otherwise = addSpace  "big big circle" ++ wrap sArea
  where area = pi * r ^ 2
        -- ! 必ず揃える必要がある。少し愚か
        --little = 10
        --normal = 50
        -- where 中でパターンマッチを使用可能
        (little, normal) = (10, 50)
        sArea = show area
        -- 関数を定義可能
        addSpace s = ' ' : s
        -- where はネスト可能。補助関数中で補助関数を定義
        wrap s = wrapLeft (wrapRight s)
          where wrapLeft s = " " ++ s
                wrapRight s = s ++ " "

where 句のいくつかの特徴:

  • 複数行宣言必ずインデントを揃える必要がある。否则コンパイラは正しく解析できない(定義する変数/関数リストが終了したか分からない)

  • 句中で宣言された変数と関数のスコープは現在の関数とその guard。同名関数の他のパターンは含まない

  • 句中でパターンマッチを使用可能

  • ネスト使用を許可。補助関数も自分の where 句で必要な変数と補助関数を宣言可能

注意。where は構文構造。関数底部で変数/関数を宣言するために使用。スコープは guard を含む関数全体

P.S. 強いて単行にする場合、セミコロンで複数の宣言を区切可能。例えば:

sayHello = hello ++ " " ++ greeting
  where hello = "Hi"; greeting = "girls"

let キーワード

構文形式:

let [bindings] in [expressions]

例えば:

cylinder r h =
    let sideArea = 2 * pi * r * h
        bottomArea = pi * r ^ 2
    in sideArea + 2 * bottomArea

-- 式形式の一般用法は JS 分割代入に類似
oneTwoThree = let (a, b, c) = (1, 2, 3) in a + b + c

let-in の作用は where と類似。どちらも局所変数/関数を定義するために使用。違いは以下の通り:

  • 形式上:let xxx in......where xxx の宣言位置の違い。let は定義を前に配置

  • 構文上:let-in は式。where は構文構造。前者はどこにでも配置可能

  • スコープ上:let-in のスコープ制限はより厳しい。let 部分で定義された変数/関数は in 部分に対してのみ可視

注意。同様に複数行宣言は厳密に揃える必要がある。強いて単行にする場合はセミコロンで区切る

P.S. let-inin 部分は省略可能。スコープは現在の関数/List Comprehension に拡張。GHCi 環境の場合、対話プロセス全体で可視

Case 式

最も一般的な case 式は関数定義時のパラメータのパターンマッチ(case 式の構文糖):

tail' [] = "empty list"
tail' [x] = "no tail"
tail' (_:xs) = show xs

等价于

tail'' xs = case xs of  [] -> "empty list"
                        [x] -> "no tial"
                        (_:xs) -> show xs

構文形式は以下の通り:

case expression of  pattern -> result
                    pattern -> result
                    pattern -> result
                    ...

expressionpattern を順に試す。マッチ成功すれば対応するコードブロックを実行して結果を返す。否则次を試す。どれもマッチしなければエラー

P.S. 同様に、式として、case-of はどこにでも使用可能。パターンマッチよりはるかに柔軟(パターンマッチは関数宣言、wherelet、List Comprehension などの特定シーンでのみ使用可能)

六.データ構造

List

Haskell の List は単一タイプ配列。例えば:

emptyArr = []
numbers = [1, 2, 3, 4]
chars = ['a', 'b', 'c']

実際、文字列は Char タイプ要素の List。例えば:

> str = "okay"
> :i str
str :: [Char] 	-- Defined at <interactive>:51:1

> charArr = ['o', 'k', 'a', 'y']
> :i charArr
charArr :: [Char] 	-- Defined at <interactive>:54:1

したがって、文字列リテラルは List 構文糖に過ぎない

List 操作

++ 接続:

> "Is life always this hard" ++ " ,or is it just when you are a kid ?"
"Is life always this hard ,or is it just when you are a kid ?"
--  または
> ['A', 'l', 'w', 'a', 'y', 's'] ++ [' '] ++ "like this."
"Always like this."

注意。++ 操作は左側の List を走査するため、パフォーマンスは高くない

: 左端挿入:

> 0 : [1, 2, 3]
[0,1,2,3]

また、[1, 2, 3] は実際 1 : 2 : 3 : [] の構文糖。: は右結合して完全な List を得る

!! インデックスで要素取得:

> "hello there" !! 4
'o'
> "hello there" !! 14
*** Exception: Prelude.!!: index too large

インデックスは 0 から開始。越界するとエラー

多次元配列

> [[1], [2, 3]] !! 1 !! 1
3

鋸歯状配列を許可。同様に要素タイプ一致を要求

List 比較

List 中の要素が比較可能であれば、List は比較可能(各要素を走査比較):

> "hello" == ['h', 'e', 'l', 'l', 'o']
True
> "0110" > "0101"
True

一般的な関数

--  head は先頭要素を取得
> head [1, 2, 3]
1
--  tail は残りを取得(先頭要素を除去)
> tail [1, 2, 3]
[2,3]
--  last は末尾要素を取得
> last [1, 2, 3]
3
--  init は本体を取得(末尾要素を除去)
> init [1, 2, 3]
[1,2]

注意:List が空の場合、これら 4 つの関数はエラー

--  length は配列長さを求める
> length [1, 2]
2
--  null は空を判断
> null []
True
--  reverse は先末尾転置
> reverse [1, 3, 2]
[2,3,1]
--  take は前 n 個の要素を取得
> take 5 [1, 2]
[1,2]
--  drop は前 n 個の要素を除去
> drop 5 [1, 2]
[]

take/drop 越界はエラーにならない

--  maximum は最大値を求める
> maximum [1, -2, 3 , 0]
3
--  sum は総和を求める
> sum [1..10]
55
--  elem は包含性を判断
> 3 `elem` [1, 2]
False

その中で [1..10] は Range 構文。elem は中綴形式で呼び出す方が意味習慣に合致

Range

列挙可能 List を生成する便利な方法。例えば:

> [1..7]
[1,2,3,4,5,6,7]
> ['A'..'Z']
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"

前 2 項を通じて間隔(step)を指定可能。負間隔(減算シーケンス)も可能:

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

浮動小数点数は精度の問題がある。Range での使用は推奨されない:

> [0.1, 0.3..1]
[0.1,0.3,0.5,0.7,0.8999999999999999,1.0999999999999999]

また、無限シーケンスも許可。例えば:

--  上限を設定しない Range
> take 3 [1..]
[1,2,3]
--  または cycle 関数で無限繰り返し
> take 7 (cycle [1..3])
[1,2,3,1,2,3,1]
--  または repeat で単一要素無限繰り返し List を生成
> take 6 (repeat 3)
[3,3,3,3,3,3]
--  前文の結果は等价于
> replicate 6 3
[3,3,3,3,3,3]

List Comprehension

リスト内包。既存 List から一定ルールに従って別の List を生成すること。そのため map, filter などの操作が必要なシーンは全て List Comprehension で完了可能

構文形式は数学集合定義と類似。例えば集合で 10 以内の偶数を記述すると S = {2 * x | x <- N, x <= 5}。対応する List Comprehension 構文は以下の通り:

> [ 2 * x | x <- [1..5] ]
[2,4,6,8,10]

P.S. <- で所属記号を表す。非常に形象的

さらに制限条件(predicate)を追加可能:

> [ 2 * x | x <- [1..5], 2 * x `mod` 3 == 1, x `mod` 5 /= 0 ]
[4]

制限条件を追加してフィルタリングする他に、let で変数/関数を宣言可能:

> [ doubleX | x <- [1..5], let doubleX =  2 * x, doubleX `mod` 3 == 1, x `mod` 5 /= 0 ]
[4]

P.S. in 部分を省略すると、宣言された変数/関数はその右側の制限条件と変数、および縦線左側部分に対して可視。付与すると、現在の条件のみに作用

もう少し複雑な例。1 から 100 までのすべての素数を求める:

isPrime n = null [ x | x <- [2..n-1], n `mod` x == 0 ]
[ x | x <- [1..100], isPrime x ]

数学公式とほとんど違いがない。isPrime の判定ルールは n2..n-1 中のどの数でも割り切れないこと。1 から 100 中で該判定ルールを満足するすべての要素で構成される集合が求めるもの

集合定義のように、要素は複数の既存集合から来ることが可能。例えば:

> [ x * y | x <- [1, 2, 3], y <- [4, 5] ]
[4,5,8,10,12,15]

List Comprehension を利用して length 関数を実装可能:

length' arr = sum [ 1 | x <- arr ]
--  またはより習慣に合致する
length' xs = sum [ 1 | _ <- xs ]

P.S. その中で _ はプレースホルダーを表す。値が何かは気にしない。一種の地道なコーディング習慣

また、List Comprehension はネスト可能:

--  二次元配列中の奇数をフィルタ
[ [ x | x <- xs, even x ] | xs <- [[1,2], [3, 4]] ]
[[2],[4]]

Tuple

タプルは単一要素タイプを要求しない。タイプ制約から見ると、構造体に相当

例えば:

> :t (1, "Leon")
(1, "Leon") :: Num t => (t, [Char])
--  List はタイプ単一を要求。そのため二元組と三元組を 1 つの List に入れるとエラー
> [(1, "Leon"), (2, "Milk"), (3, "Little", "Girl")]
<interactive>:148:28: error:
    ? Couldn't match expected type '(t, [Char])'
                  with actual type '(Integer, [Char], [Char])'

List と同じく、タプル中の要素が比較可能であれば、同タイプタプルも比較可能

もう少し複雑な例。三辺の長さが全て整数で 10 以下、周囲長が 24 の直角三角形をすべて求める:

[ (a, b, c) | c <- [1..10], b <- [1..c], a <- [1..b], a + b + c == 24, a^2 + b^2 == c^2 ]

その中に隠された辺長関係に注意:c >= b >= a。重複除去ルールとして計算

一般的な関数

fst/snd で二元組の先頭要素/末尾要素を取得:

fst (1, 2)
snd (1, 2)

注意。これら 2 つの関数は二元組にのみ使用可能。一般タプルには類似のツール関数がない。しかしパターンマッチを通じて自分で実装可能:

-- 三元組の先頭要素を取得
first (x, _, _) = x

zip で List からタプルを組み合わせ:

> zip [1, 2] ['A', 'B', 'C']
[(1,'A'),(2,'B')]

余分な単身要素は捨てられる

参考資料

コメント

コメントはまだありません

コメントを書く