본문으로 건너뛰기

기초 문법_Haskell 노트 1

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

이 매우 순수한 함수형 언어를 다시 집어들고, 그것을 좋아하기 시작하다

一.簡介

Haskell 은 순수 함수형 언어 (purely functional programming language) 이며, 그 함수형 특성의 순수성에 이의는 없습니다

명령형 언어는求解의 스텝을 제공할 것을 요구하지만, Haskell 은 문제의記述을 제공할 것을 傾向으로 한다

  • 비함수형 사고:명령을 통해 컴퓨터에 무엇을 할지를 지시한다. 예를 들어 총합은 루프 구조를 통해 모든 수치를 주사하고, 가산하여 그 합을 기록한다

  • 함수형 사고:함수를 통해 문제가 무엇인지記述한다. 예를 들어 총합은 첫 번째 수치와 나머지의 합을 가산한다

P.S. 사고 모드의 차이에 대해서는, [一場함수형 사고 모드의 세례](/articles/一場함수형 사고 모드의 세례/) 참조

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 리터럴:True, False

等價性 判斷

  • ==:같은지 判斷. 文字列에 사용 가능

  • /=:같지 않은지 判斷 (數學者의 言語. 따라서不等號는 이렇게 보인다)

注意:타입은 嚴密하게 一致하지 않으면 比較할 수 없다. 否则 에러로 比較 可能性이 없다고 간주된다 (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. 또 하나 재미있는 것이 있다: ($ 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 는 어디에나 使用 가능. 패턴매치보다는 훨씬 柔軟 (패턴매치는 函數 宣言, where, let, 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')]

餘分한 單身 要素는 버려진다

參考 資料

댓글

아직 댓글이 없습니다

댓글 작성