📖Haskellを理解する
Haskellをちょっとだけ理解します。
# 単発実行
runghc learn/01_basics.hs
# REPL でいじる(推奨)
ghci learn/01_basics.hs
# その1
- Haskellではすべての関数が再帰的にカリー化されている
-- 1. 関数定義と型注釈
--
-- 「型注釈 :: 型」は省略可能だが、書くと読み手にもコンパイラにも親切。
-- 関数は「f 引数1 引数2 = 式」の形。括弧やカンマは要らない。
square :: Integer -> Integer
square x = x * x
-- 複数引数。実体は「Integer -> (Integer -> Integer)」のカリー化。
addInt :: Integer -> Integer -> Integer
fst2 :: (a, b) -> aこれって引数と戻り値の型宣言してる?- -> してる
fst2 :: forall a b. (a, b) -> aの省略形- 例えば、次の場合に省略できる
- トップレベルの
::で、小文字始まりの型変数を シグネチャの中だけで使う
- トップレベルの
-- ---------------------------------------------------------------------
-- 2. パターンマッチ
-- ---------------------------------------------------------------------
--
-- 引数の「形」で分岐する。上の節から順に試して最初に合った節が選ばれる。
-- 本体コードでは `add _ Infinity q = q` などで使われている。
-- タプルを分解する例
fst2 :: (a, b) -> a
fst2 (x, _) = x -- _ は「使わないので名前を付けない」
-- 数値リテラルでもマッチできる
describe :: Integer -> String
describe 0 = "zero"
describe 1 = "one"
describe _ = "many"
ghci> describe 0
"zero"
ghci> describe 1
"one"
ghci> describe 2
"many"
Note
関数定義を、引数ごとに個別に書くことができる。
_ はワイルドカードであり、全てにマッチする。
describe :: Integer -> String
describe 0 = "zero"
describe _ = "many"
describe 1 = "one" -- 永遠に呼ばれない (dead code)
-- ---------------------------------------------------------------------
-- 3. ガード ( | 条件 = 式 )
-- ---------------------------------------------------------------------
--
-- if-then-else より読みやすい多分岐。otherwise は単に True の別名。
-- src/ECC/Curve.hs の `add` `double` で多用されている書き方。
sign :: Integer -> Integer
sign n
| n > 0 = 1
| n < 0 = -1
| otherwise = 0
let ~ inとwhereの違いlet ~ inは式なのでgoukei = 10 * (let num = 10 in num + 2)みたいな使い方ができるwhereは節 (ブロック?) なのでできない
-- ---------------------------------------------------------------------
-- 4. let と where
-- ---------------------------------------------------------------------
--
-- どちらも「式の途中で使う名前」を導入する。
-- * let は式の中、where は関数定義の脇。
-- Curve.hs では `let s = ... ; x3 = ... in Affine x3 y3` の形がよく出る。
hypotenuse :: Double -> Double -> Double
hypotenuse a b =
let aa = a * a
bb = b * b
in sqrt (aa + bb)
-- 同じ意味を where で書くと:
hypotenuse' :: Double -> Double -> Double
hypotenuse' a b = sqrt (aa + bb)
where
aa = a * a
bb = b * b
- そもそも型の宣言?
data Hoge = ...が新しい型の宣言...に、この型を作るための方法を定義する (値コンストラクタ)
- 値コンストラクタ
Point2D Double Doubleは、Double -> Double -> Point2Dの関数
deriving (Eq, Show)EqやShowの実装をコンパイラに丸投げする
data Point2D
= Origin -- 値コンストラクタ (引数なし)
| Point2D Double Double -- 値コンストラクタ (Double を 2 つ持つ)
deriving (Eq, Show)
-- ---------------------------------------------------------------------
-- 5. 代数的データ型 (Algebraic Data Type, ADT)
-- ---------------------------------------------------------------------
--
-- 「合計型」 = いくつかの形のうちいずれかをとる型。
-- 本体の Point は「Infinity または Affine x y」という ADT。
--
-- data Point p = Infinity
-- | Affine (Fp p) (Fp p)
--
-- ここでは Fp を使わない簡易版を作る。
-- 「2次元の点 (またはどこにも無い)」を表す型。
data Point2D
= Origin -- 値コンストラクタ (引数なし)
| Point2D Double Double -- 値コンストラクタ (Double を 2 つ持つ)
deriving (Eq, Show)
-- deriving = よくある型クラスのインスタンスを自動生成。
-- Eq があれば (==), Show があれば print できる。
-- パターンマッチで分解して使う。
distanceFromOrigin :: Point2D -> Double
distanceFromOrigin Origin = 0
distanceFromOrigin (Point2D x y) = sqrt (x * x + y * y)
-- 2 点の加算 (ベクトル和)。Curve.hs の `add` と同じ「両端の場合分け」を真似ている。
add2D :: Point2D -> Point2D -> Point2D
add2D Origin q = q -- 単位元 + q = q
add2D p Origin = p -- p + 単位元 = p
add2D (Point2D x1 y1) (Point2D x2 y2) =
Point2D (x1 + x2) (y1 + y2)
# その2
newtype- フィールドが一つだけのdataを作るときに使える
- いくつかの嬉しさがあるが、今回は気にしなくて良さそう
{ unMod7 :: Integer }はレコード構文- フィールドに名前をつけたいときに使う書き方
- 本質的には
data Mod7 = Mod7 Integerと一緒 -
unMod7 :: Mod7 -> Integer が自動で生える。- これは、Integerフィールドのgetterに
unMod7という名前をつけた、ということ
- これは、Integerフィールドのgetterに
-- ---------------------------------------------------------------------
-- 1. newtype vs data
-- ---------------------------------------------------------------------
--
-- newtype は「フィールド 1 つだけの data」専用。
-- 実行時には中身そのものに見え、ラッピングのコストがない。
-- src/ECC/Field.hs:
-- newtype Fp (p :: Nat) = Fp { unFp :: Integer }
--
-- ここでは「7 を法とする整数」を表す Mod7 を作る。
-- 本体では p を型レベルで持つが、まずは p = 7 を直接埋め込んだ簡易版で慣れる。
newtype Mod7 = Mod7 { unMod7 :: Integer }
-- { フィールド名 :: 型 } を書くと、unMod7 :: Mod7 -> Integer が自動で生える。
-- これは Field.hs の `unFp :: Fp p -> Integer` と同じ仕組み。
x 'mod' 7(本当はバッククォート)- 中置記法
mod x 7と同値- 逆に、演算子
+などを(+)のように(..)で書くと、前置関数として使える(+) 1 2と1 + 2は同値
-- ---------------------------------------------------------------------
-- 2. スマートコンストラクタ
-- ---------------------------------------------------------------------
--
-- Mod7 のコンストラクタは任意の Integer を受け取れてしまう。
-- 「代表元は [0, 7) に正規化されている」という不変条件を守るために、
-- 作るときは必ずこの関数を通す。Field.hs の mkFp と同じ役目。
mkMod7 :: Integer -> Mod7
mkMod7 x = Mod7 (x `mod` 7)
instanceの意味が、オブジェクト指向のプログラミング言語とは違う?- Haskellの
instanceは、「この型 (Mod7) はこの型クラス (EqやShow) の条件を満たす」という宣言 - Interface に近い
- Haskellの
-- ---------------------------------------------------------------------
-- 3. Eq / Show のインスタンス
-- ---------------------------------------------------------------------
--
-- 多くの場合は `deriving (Eq, Show)` で十分。
-- ここでは「自分で書くとはどういうことか」を見るために手書きする。
instance Eq Mod7 where
Mod7 a == Mod7 b = a == b
-- 左辺の演算子 == は Mod7 -> Mod7 -> Bool として定義中。
-- 右辺の == は Integer の (==) (組み込みの Eq Integer)。
instance Show Mod7 where
show (Mod7 a) = "Mod7 " <> show a
Example
Eq 型クラスは、標準ライブラリで次のような定義をされている。
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
Numインスタンスnegateは和についての逆元を返すabsは絶対値を返すidは恒等写像
signumは符号を返すfromIntegerは数字リテラル (1,2,3,...) を書いてMod7型を作れるようにする-
型推論で Mod7 にもなれる
-
Example
ghci> :type 5
5 :: Num a => a
ghci> :type (5 :: Mod7)
(5 :: Mod7) :: Mod7
-- ---------------------------------------------------------------------
-- 4. Num インスタンス: + - * negate fromInteger
-- ---------------------------------------------------------------------
--
-- 「型クラス Num」のメソッドを実装すると、+ - * といった演算子が
-- その型に対しても使えるようになる。Haskell の四則演算は全て型クラス経由。
--
-- 本体 Field.hs:
-- instance KnownNat p => Num (Fp p) where
-- Fp a + Fp b = mkFp (a + b)
-- ...
instance Num Mod7 where
Mod7 a + Mod7 b = mkMod7 (a + b) -- 加えてから mod
Mod7 a - Mod7 b = mkMod7 (a - b)
Mod7 a * Mod7 b = mkMod7 (a * b)
negate (Mod7 a) = mkMod7 (negate a)
abs = id -- 0..6 しか入っていないので絶対値は自分自身
signum (Mod7 0) = Mod7 0
signum _ = Mod7 1
fromInteger n = mkMod7 n
-- fromInteger を入れておくと、コード中の数値リテラル 0, 1, ... が
-- 型推論で Mod7 にもなれる。下の main で a + 1 と書けるのはこのおかげ。
-- ---------------------------------------------------------------------
-- 5. 「型クラス制約」のついた関数
-- ---------------------------------------------------------------------
--
-- 引数の型に「Num の能力がある」と要求する関数。
-- Mod7 でも Integer でも Double でも動く多相な関数になる。
-- Field.hs の `powFp :: KnownNat p => Fp p -> Integer -> Fp p` も
-- 「KnownNat p の能力がある」という同じ書き方。
square :: Num a => a -> a
square x = x * x
-- 試しに何回か足す関数 (Num の + が使える型ならなんでも OK)
triple :: Num a => a -> a
triple x = x + x + x
# その3
- 型レベル自然数
- 数値リテラルを値ではなく型として使える仕組み
- 例えば、
23という数値リテラルがNatという Kind を持つ
- Kind とは
newtype ModN (n :: Nat) = ModN { unModN :: Integer }(n :: Nat)Natの制約下で型変数nを受け取るModNはNat -> *
Note
この ModN は「値から値を作る関数」とも「型から型を作る関数」とも見れる。というか、その2つを、同時に定義しているっぽい。「1つの ModN を別の見方ができる」より、「2つの ModN を同時に定義した」と思ったほうが良さそう。
ghci> :kind ModN
ModN :: Nat -> *
ghci> :type ModN
ModN :: Integer -> ModN n
ghci> :type ModN 7
ModN 7 :: ModN n
-- ---------------------------------------------------------------------
-- 1. 型レベル自然数を引数に取る newtype
-- ---------------------------------------------------------------------
--
-- Fp と同じ構造。法 n を「型」のところに書いていることに注目。
-- ModN 7 と ModN 23 はコンパイラから見てまったく別の型になる。
newtype ModN (n :: Nat) = ModN { unModN :: Integer }
deriving (Eq)
instance Show (ModN n) where
show (ModN x) = "ModN " <> show x
->と=>KnownNat- GHC.TypeNats
- 「型レベル自然数を実行時に値として取り出せる」型クラス
natValKnownNatとしてnを受け取り、Naturalとしてnを返す
class KnownNat (n :: Nat) where
natSing :: SNat n
-- | @since 4.10.0.0
natVal :: forall n proxy. KnownNat n => proxy n -> Natural
natVal _ = case natSing :: SNat n of
UnsafeSNat x -> x
proxy- ここでは
Nat -> *想定の型変数
- ここでは
Proxy
data Proxy a = Proxy
modulusOf _ = toInteger (natVal (Proxy @n))@nはProxyの型変数aにnをバインドする@nのことを TypeApplications という
Question
なぜわざわざ @n という書き方をするのか?引数に ModN n 型の値をとって、逆算して n にアクセスすることもできるはず。
-- ---------------------------------------------------------------------
-- 2. 型レベル -> 値レベル : natVal で n を取り出す
-- ---------------------------------------------------------------------
--
-- Field.hs の modulus とまったく同じ作り。
-- * forall n. : 型変数 n を関数本体でも使うと宣言
-- * KnownNat n : 「n は実行時に Integer として取れる」制約
-- * Proxy @n : 「n という型」を値として運ぶダミー (中身は無い)
-- * natVal : Proxy から実際の自然数 (Natural) を取り出す
--
-- ModN n の値そのものは引数として使わず捨てている (`_`)。
-- これは「型 n に依存して値を出している」ということ。
modulusOf :: forall n. KnownNat n => ModN n -> Integer
modulusOf _ = toInteger (natVal (Proxy @n))
mkMod @23 45のように実行する- ->
ModN 23型の値22を返す
- ->
-- ---------------------------------------------------------------------
-- 3. スマートコンストラクタ
-- ---------------------------------------------------------------------
--
-- 型から法を取って正規化する。Field.hs の mkFp と同じ。
mkMod :: forall n. KnownNat n => Integer -> ModN n
mkMod x = ModN (x `mod` toInteger (natVal (Proxy @n)))
KnownNat nに対して、ModN nをNumインスタンスにするmkModには@nを付けていないが、型推論がうまく動いている- 「この
Num契約の宣言はModN nに対してのものだから、各フィールド (関数?メソッド?) の戻り値はModN n型であるはず。なので、mkModのnも同じnになるはず」
- 「この
-- ---------------------------------------------------------------------
-- 4. Num インスタンス
-- ---------------------------------------------------------------------
--
-- 02_typeclass.hs の Mod7 と同じだが、法 n が型に乗っているので
-- KnownNat n 制約をインスタンスに付けてある。
-- Field.hs の `instance KnownNat p => Num (Fp p)` と同じ形。
instance KnownNat n => Num (ModN n) where
ModN a + ModN b = mkMod (a + b)
ModN a - ModN b = mkMod (a - b)
ModN a * ModN b = mkMod (a * b)
negate (ModN a) = mkMod (negate a)
abs = id
signum (ModN 0) = ModN 0
signum _ = ModN 1
fromInteger = mkMod
ちゃんと違う ModN n 同士は足せないようになっている。
ghci> a = mkMod @7 1
ghci> b = mkMod @23 1
ghci> a + b
<interactive>:10:5: error: [GHC-83865]
• Couldn't match type ‘23’ with ‘7’
Expected: ModN 7
Actual: ModN 23
• In the second argument of ‘(+)’, namely ‘b’
In the expression: a + b
In an equation for ‘it’: it = a + b
-- ---------------------------------------------------------------------
-- 5. 型の付け方 (重要)
-- ---------------------------------------------------------------------
--
-- ModN n の n は通常の型推論では決まらないので、`:: ModN 23` のように
-- 型注釈で明示する。または TypeApplications で `mkMod @23 100` と書く。
--
-- 試しに ghci でやってみると違いがよく分かる:
-- ghci> mkMod 100 :: ModN 23 -- ModN 8
-- ghci> mkMod @23 100 -- ModN 8 (同じ)
-- ghci> mkMod 100 :: ModN 7 -- ModN 2
-- 型エラーの実演 (コメントアウト):
-- 違う n 同士は足せない (ことを試したい場合は↓のコメントを外す)
-- broken = (mkMod 1 :: ModN 7) + (mkMod 1 :: ModN 23)
-- -- "Couldn't match type '7' with '23'" のようなエラーになる。
-- -- これが「型でうっかり混ぜないことを保証する」効果。
# Exercise
CCに理解度チェックを作らせて、自分で実装した。
-- =====================================================================
-- Part A: Color (ADT / パターンマッチ / ガード)
-- =====================================================================
--
-- 要件:
--
-- (1) data Color を定義してください。値コンストラクタは以下の 4 つ:
-- - Red
-- - Green
-- - Blue
-- - Custom Int Int Int -- (R, G, B の輝度を 0..255 で持つ)
-- Eq, Show を deriving すること。
--
-- (2) brightness :: Color -> Int を実装してください。
-- - Red, Green, Blue: それぞれ 255
-- - Custom r g b: (r + g + b) を 3 で割った商 (Int の整数除算)
--
-- (3) describe :: Color -> String を実装してください (ガードを使う):
-- - Red -> "red"
-- - Green -> "green"
-- - Blue -> "blue"
-- - Custom r g b で brightness の結果が:
-- - 200 以上 -> "bright custom"
-- - 100 以上 -> "medium custom"
-- - それ未満 -> "dark custom"
--
-- ↓ ここから実装してください
data Color = Red | Green | Blue | Custom Int Int Int deriving (Eq, Show)
brightness :: Color -> Int
brightness Red = 255
brightness Green = 255
brightness Blue = 255
brightness (Custom r g b) = (r + g + b) `div` 3
describe :: Color -> String
describe Red = "red"
describe Green = "green"
describe Blue = "blue"
describe (Custom r g b)
| bright >= 200 = "bright custom"
| bright >= 100 = "medium custom"
| otherwise = "dark custom"
where
bright = brightness (Custom r g b)
-- =====================================================================
-- Part B: Clock (n :: Nat) — n 時間時計
-- =====================================================================
--
-- 「n 時間でちょうど一周する時計」を表す型を作ります。
-- たとえば Clock 12 なら 12 時間で 1 周、Clock 7 なら 7 でリセット。
--
-- 要件:
--
-- (1) newtype Clock (n :: Nat) を定義してください。
-- 内部表現は Integer、getter (アクセサ) の名前は unClock とすること。
-- Eq は deriving で OK。
--
-- (2) mkClock :: KnownNat n => Integer -> Clock n を実装してください。
-- 入力を [0, n) の範囲に正規化して Clock に詰める。
-- (natVal と Proxy @n を使って n を取り出す)
--
-- (3) clockMod :: KnownNat n => Clock n -> Integer を実装してください。
-- 時計の n (周期) を Integer として返す。
--
-- (4) Show インスタンスを手書きで実装してください。
-- フォーマットは厳密に "Clock <値>" とすること。
-- 例: show (mkClock @12 5) == "Clock 5"
--
-- (5) Num インスタンス (KnownNat n が必要) を実装してください:
-- - (+), (-), (*): mkClock を使って結果を [0, n) に正規化
-- - negate: mkClock を使って正規化
-- - abs: id
-- - signum: 中身が 0 なら 0、そうでなければ 1
-- - fromInteger: mkClock そのまま
--
-- (6) tickN :: KnownNat n => Integer -> Clock n -> Clock n
-- 時計を N 時間進める。Num の (+) を使うか mkClock で再構築する。
--
-- ↓ ここから実装してください
-- Clock のスケルトン (テストが unClock を参照するので名前は固定)
newtype Clock (n :: Nat) = Clock {unClock :: Integer}
deriving (Eq)
mkClock :: forall n. (KnownNat n) => Integer -> Clock n
mkClock t = Clock (t `mod` toInteger (natVal (Proxy @n)))
clockMod :: forall n. (KnownNat n) => Clock n -> Integer
clockMod _ = toInteger (natVal (Proxy @n))
instance (KnownNat n) => Show (Clock n) where
show c = "Clock " ++ show (unClock c)
instance (KnownNat n) => Num (Clock n) where
(+) a b = mkClock (unClock a + unClock b)
(-) a b = mkClock (unClock a - unClock b)
(*) a b = mkClock (unClock a * unClock b)
negate a = mkClock (-unClock a)
abs = id
signum (Clock a)
| a == 0 = mkClock 0
| otherwise = mkClock 1
fromInteger = mkClock
tickN :: (KnownNat n) => Integer -> Clock n -> Clock n
tickN t c = c + mkClock t