Logo

Nola's Backyard

📖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 という名前をつけた、ということ
-- ---------------------------------------------------------------------
-- 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 に近い
-- ---------------------------------------------------------------------
-- 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 とは
    • 型を分類するための型
    • 例えば、
      • * または Type
        • 普通の型 (Int, Bool, Mod7, ...)の kind
      • Nat
        • 型レベル自然数 (0, 1, 23, ...) の kind
      • * -> *
        • 「型を一つとって型を返す」コンストラクタ (Maybe, [], IO, ...) の 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


  • -> と =>
    • -> は型から型への関数の矢印
    • => は型変数に対する「型クラスの制約」と「型の定義」を区切る矢印
      • => の左が制約、右が本来の型
      • 例えば square :: Num a => a -> a は
        • 「square は Num a という制約のもとで、a -> a という型を持つ」という意味
  • KnownNat
  • natVal
    • KnownNat として 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
  • Proxy
    • k -> Type
      • k は何でもOKで、今回は型の想定?
    • 型情報だけを運ぶ、中身ゼロのダミー値
    • Haskellでは引数に型を渡す事はできないためこれを使う
data Proxy a = Proxy
  • modulusOf _ = toInteger (natVal (Proxy @n))
    • @n は Proxy の型変数 a に n をバインドする
      • つまり、Proxy @n は Proxy n 型の値になる
      • ScopedTypeVariables 拡張のおかげで、型シグネチャの n を本体側でも参照できる
    • @n のことを TypeApplications という
Question

なぜわざわざ @n という書き方をするのか?引数に ModN n 型の値をとって、逆算して n にアクセスすることもできるはず。

  • 関数を値引き数なしにできる (今回はしてない)
  • 「値を使わない」という意思の明示
  • これから値を作る場合
    • mkFp :: forall p. KnownNat p => Integer -> Fp p
      • 型変数を戻り値の型のために使う場合、「引き数から型変数を逆算する」ことはできない
-- ---------------------------------------------------------------------
-- 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


# 詰まったところ

  • forall n. を書かないと Proxy @n で n が見えない
  • Integer には / はなくて、div を使う
  • 文字列の足し算は + ではなく ++
  • 関数の定義における引数の扱い
    • signum (Clcok a) = ... なら a に入るのは Clock ではなくて Integer
      • Clock として扱いたければ再度 Clock a として Clock 型の値を生成する必要がある
    • show c = "Clock " ++ show (unClock c) と、直接 Clcok 型を受け取る書き方もできる