Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save lagenorhynque/4f139d0e6ef338c899fe55c80674e837 to your computer and use it in GitHub Desktop.
Save lagenorhynque/4f139d0e6ef338c899fe55c80674e837 to your computer and use it in GitHub Desktop.

関数型言語テイスティング

Haskell, Scala, Clojure, Elixirを

比べて味わう関数型プログラミングの旨さ


x icon


[参考] 🐬の関数型言語学習/利用経験

most used languages


  • 関数型言語経験: 約12年
    • cf. プログラミング経験: 約13年

    • プログラミング学習初期から関数型言語と戯れてきた(そして非関数型言語に未だに馴染めない😇)

    • 仕事で: Clojure, Scala

    • 趣味で: Clojure, Haskell, Erlang/Elixir, etc.



『7つの言語 7つの世界』

seven languages in seven weeks

オーム社の書籍詳細ページより

『すごいHaskellたのしく学ぼう!』(すごいH本)

learn you a haskell for great good

オーム社の書籍詳細ページより

『プログラミングHaskell』

programming in haskell

オーム社の書籍詳細ページより

『プログラミングClojure 第2版』

programming clojure

オーム社の書籍詳細ページより

『すごいErlangゆかいに学ぼう!』(すごいE本)

learn you some erlang for great good

達人出版会の書籍詳細ページより

『Scala関数型デザイン&プログラミング』(FP in Scala)

fp in scala

インプレスブックスの書籍詳細ページより

Structure and Interpretation of Computer Programs (SICP, 魔術師本)

sicp

Wikipedia英語版のStructure and Interpretation of Computer Programsより
  1. きっかけ

  2. 「関数型プログラミング」

  3. 🍷 テイスティング

    1. 4つの関数型言語の基本
    2. 関数
    3. データ
    4. 評価
    5. ポリモーフィズム
  4. 関数型プログラマのメンタルモデル


1. きっかけ


関数型プログラミング(言語)に関する解説で

🐬は以下のような表現をよく目にする:

近年、関数型プログラミング(言語)のエッセンスが多くの主流言語に取り入れられてきました。


関数型プログラミング(言語)の

「エッセンス」??? 🤔


エッセンス(essence)

  • 語源: 🇬🇧 essence < essentia < esse + -ia (≒ 🇬🇧 beingness, あるということ)
  • 語義: 「本質(的に重要なもの)」
    • 抽出物(エキス)という派生的/比喩的な意味も
    • 本来、単なる成分/要素のような意味ではない
  • cf. 🇫🇷 哲学の文脈で関連する表現
    • l'existence précède l'essence (実存は本質に先立つ)
    • L'essentiel est invisible pour les yeux. (🦊「大切なものは目に見えないんだよ。」)

ワイン🍷テイスティングでは色(視覚)・香り(嗅覚)・味(味覚)から吟味するように、

4つの関数型言語を例にその味わいを多角的に探り、「エッセンス」(= 本質)を見出したい

関数型言語テイスティング


2. 「関数型プログラミング」


(現在の)🐬によるFPとFPLの定義

  • 関数型プログラミング := 純粋関数を基本要素としてその組み合わせによってプログラムを構成していくプログラミングスタイル

    • → 言語を問わず実践可能(実践しやすさは異なる)
  • 関数型言語 := 関数型プログラミングが言語/標準ライブラリレベルで十分に支援される(そして関数型プログラミングスタイルがユビキタスな)言語

    • → 例えばJavaScript/TypeScriptやJava、Kotlin、古典的なLisp方言は含めない

functional programming concept map

※ 🐬が思い浮かぶ概念/用語を連想的に列挙したもの(網羅的でも体系的でもない)

functional programming concept map (values)


functional programming concept map (languages)


functional programming concept map (theories)


functional programming concept map (implementations)


functional programming concept map (patterns)


3. 🍷 テイスティング

  1. 4つの関数型言語の基本
  2. 関数
  3. データ
  4. 評価
  5. ポリモーフィズム

3-1. 🍷 4つの関数型言語の基本

Haskell, Scala, Clojure, Elixir


functional programming concept map (languages)


Haskell Scala Clojure Elixir
paradigm FP OOP, FP FP FP
typing static static dynamic dynamic
first appeared 1990 2004 2007 2012
related ML ML Lisp Lisp
🐬's note lazy/pure FPL standard OOPL in FPL's skin modern functional Lisp Erlang + Ruby + Clojure

モジュール定義とトップレベル変数

{- Haskell -}
-- モジュールとそのエクスポートリスト
module SomeModule(a) where
-- パブリック変数
a :: Int
a = 1
-- プライベート変数
b :: Int
b = 2
/* Scala */
// (シングルトン)オブジェクト
object SomeModule:
  // パブリック変数
  val a: Int = 1
  // プライベート変数
  private val b: Int = 2

;;; Clojure
;; 名前空間
(ns some-module)
;; パブリック変数
(def a 1)
;; プライベート変数
(def ^:private b 2)
## Elixir
# モジュール
defmodule SomeModule do
  # パブリックな0引数関数
  def a, do: 1
  # プライベートな0引数関数
  defp b, do: 2
end

トップレベル関数

{- Haskell -}
module SimpleMath(square) where
-- パブリック関数
square :: Int -> Int
square n = n * n
-- プライベート関数
double :: Int -> Int
double n = n * 2
/* Scala */
object SimpleMath:
  // パブリックメソッド
  def square(n: Int): Int =
    n * n
  // プライベートメソッド
  private def double(n: Int): Int =
    n * 2

;;; Clojure
(ns simple-math)
;; パブリック関数
(defn square [n] (* n n))
;; プライベート関数
(defn- double' [n] (* n 2))
## Elixir
defmodule SimpleMath do
  # パブリック関数
  def square(n), do: n * n
  # プライベート関数
  defp double(n), do: n * 2
end

モジュールの利用

{- Haskell: ghciコマンドによるREPL -}
λ> :l SimpleMath
...
λ> import qualified SimpleMath as M  -- モジュールを別名で参照
λ> M.square 3
9
it :: Int
λ> map M.square [0..10]
[0,1,4,9,16,25,36,49,64,81,100]
it :: [Int]
/* Scala: scalaコマンドによるREPL */
scala> :l SimpleMath.scala
// defined object SimpleMath
scala> import SimpleMath as M  // オブジェクトを別名で参照
scala> M.square(3)
val res0: Int = 9
scala> (0 to 10).map(M.square)
val res1: IndexedSeq[Int] = Vector(0, 1, 4, 9, 16, 25, 36,
  49, 64, 81, 100)

;;; Clojure: cljコマンドによるREPL
user=> (clojure.main/load-script "simple_math.clj")
#'simple-math/double'
user=> (require '[simple-math :as m])  ; 名前空間を別名で参照
nil
user=> (m/square 3)
9
user=> (map m/square (range 0 (inc 10)))
(0 1 4 9 16 25 36 49 64 81 100)
## Elixir: iexコマンドによるREPL
# `iex simple_math.ex` でファイルを読み込んで起動
iex(1)> alias SimpleMath, as: M  # モジュールを別名で参照
SimpleMath
iex(2)> M.square(3)
9
iex(3)> Enum.map(0..10, &M.square/1)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

ローカル束縛(変数)

{- Haskell -}
λ> :{  -- REPLでの複数行入力の開始
λ| -- ML系言語でお馴染み(?)のlet式
λ| let x = 5
λ|     y = M.square(x)
λ| in "square " ++ show x ++ " = " ++ show y
λ| :}  -- REPLでの複数行入力の終了
"square 5 = 25"
it :: [Char]
/* Scala */
scala> val x = 5
val x: Int = 5
scala> val y = M.square(x)
val y: Int = 25
scala> s"square($x) = $y"
val res2: String = square(5) = 25

;;; Clojure
;; Lisp系言語でお馴染み(?)のlet式
user=> (let [x 5
             y (m/square x)]
         (str "(square " x ") = " y))
"(square 5) = 25"
## Elixir
iex(4)> x = 5
5
iex(5)> y = M.square(x)
25
iex(6)> "square(#{x}) = #{y}"
"square(5) = 25"

無名関数(ラムダ式)

{- Haskell -}
λ> (\n -> n * n * n) 2
8
it :: Num a => a
λ> map (\n -> n * n * n) [0..10]
[0,1,8,27,64,125,216,343,512,729,1000]
it :: (Num b, Enum b) => [b]
/* Scala */
scala> ((n: Int) => n * n * n)(2)
val res0: Int = 8
scala> (0 to 10).map(n => n * n * n)
val res1: IndexedSeq[Int] = Vector(0, 1, 8, 27, 64, 125, 216,
  343, 512, 729, 1000)

;;; Clojure
user=> ((fn [n] (* n n n )) 2)
8
user=> (#(* % % %) 2)  ; 無名関数の略記法
8
user=> (map (fn [n] (* n n n )) (range 0 (inc 10)))
(0 1 8 27 64 125 216 343 512 729 1000)
user=> (map #(* % % %) (range 0 (inc 10)))
(0 1 8 27 64 125 216 343 512 729 1000)
## Elixir
iex(1)> (fn n -> n * n * n end).(2)
8
iex(2)> (&(&1 * &1 * &1)).(2)  # 無名関数の略記法
8
iex(3)> Enum.map(0..10, fn n -> n * n * n end)
[0, 1, 8, 27, 64, 125, 216, 343, 512, 729, 1000]
iex(4)> Enum.map(0..10, &(&1 * &1 * &1))
[0, 1, 8, 27, 64, 125, 216, 343, 512, 729, 1000]

再帰と無限リスト(1): 階乗

{- Haskell -}
module Factorial where

factorial :: Integral a => a -> a
factorial 0 = 1
factorial n = n * factorial (n - 1)

factorialSeq :: Integral a => [a]
factorialSeq = scanl (*) 1 [1..]
-- import Factorial (factorialSeq)
λ> take 10 factorialSeq
[1,1,2,6,24,120,720,5040,40320,362880]
it :: Integral a => [a]
λ> factorialSeq !! 100
9332621544394415268169923885626670049071596826438162146859296
3895217599993229915608941463976156518286253697920827223758251
185210916864000000000000000000000000
it :: Integral a => a

/* Scala */
object Factorial:
  def factorial(n: Int): BigInt = n match
    case 0 => 1
    case _ => n * factorial(n - 1)

  def factorialSeq: LazyList[BigInt] =
    LazyList.from(1).scanLeft(BigInt(1))(_ * _)
// import Factorial.factorialSeq
scala> factorialSeq.take(10).toList
val res0: List[BigInt] = List(1, 1, 2, 6, 24, 120, 720, 5040,
  40320, 362880)
scala> factorialSeq(100)
val res1: BigInt = 933262154439441526816992388562667004907159
6826438162146859296389521759999322991560894146397615651828625
3697920827223758251185210916864000000000000000000000000

;;; Clojure
(ns factorial)

(defn factorial [n]
  (if (zero? n)
    1
    (*' n (factorial (dec' n)))))

(defn factorial-seq []
  (reductions *' 1 (iterate inc' 1)))
;; (require '[factorial :refer [factorial-seq]])
user=> (take 10 (factorial-seq))
(1 1 2 6 24 120 720 5040 40320 362880)
user=> (nth (factorial-seq) 100)
9332621544394415268169923885626670049071596826438162146859296
3895217599993229915608941463976156518286253697920827223758251
185210916864000000000000000000000000N

## Elixir
defmodule Factorial do
  def factorial(0), do: 1
  def factorial(n), do: n * factorial(n - 1)

  def factorial_seq do
    Stream.concat(
      [1],
      Stream.scan(Stream.from_index(1), &*/2)
    )
  end
end
# import Factorial, only: [factorial_seq: 0]
iex(2)> Enum.take(factorial_seq, 10)
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880]
iex(3)> Enum.at(factorial_seq, 100)
9332621544394415268169923885626670049071596826438162146859296
3895217599993229915608941463976156518286253697920827223758251
185210916864000000000000000000000000

再帰と無限リスト(2): フィボナッチ数

{- Haskell -}
module Fibonacci where

fib :: Integral a => a -> a
fib 0 = 0
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)

fibSeq :: Integral a => [a]
fibSeq = map fst $ iterate (\(a, b) -> (b, a + b)) (0, 1)
-- import Fibonacci (fibSeq)
λ> take 10 fibSeq
[0,1,1,2,3,5,8,13,21,34]
it :: Integral a => [a]
λ> take 3 $ drop 100 fibSeq
[354224848179261915075,573147844013817084101,
  927372692193078999176]
it :: Integral a => [a]

/* Scala */
object Fibonacci:
  def fib(n: Int): BigInt = n match
    case 0 => 0
    case 1 => 1
    case _ => fib(n - 1) + fib(n - 2)

  def fibSeq: LazyList[BigInt] =
    LazyList.iterate((BigInt(0), BigInt(1))) {
      case (a, b) => (b, a + b)
    }.map(_(0))
// import Fibonacci.fibSeq
scala> fibSeq.take(10).toList
val res2: List[BigInt] = List(0, 1, 1, 2, 3, 5, 8, 13, 21,
  34)
scala> fibSeq.drop(100).take(3).toList
val res3: List[BigInt] = List(354224848179261915075,
  573147844013817084101, 927372692193078999176)

;;; Clojure
(ns fibonacci)

(defn fib [n]
  (case n
    0 0
    1 1
    (+' (fib (-' n 1))
        (fib (-' n 2)))))

(defn fib-seq []
  (->> [0 1]
       (iterate (fn [[a b]] [b (+' a b)]))
       (map first)))
;; (require '[fibonacci :refer [fib-seq]])
user=> (take 10 (fib-seq))
(0 1 1 2 3 5 8 13 21 34)
user=> (->> (fib-seq) (drop 100) (take 3))
(354224848179261915075N 573147844013817084101N
  927372692193078999176N)

## Elixir
defmodule Fibonacci do
  def fib(0), do: 0
  def fib(1), do: 1
  def fib(n), do: fib(n - 1) + fib(n - 2)

  def fib_seq do
    {0, 1}
    |> Stream.iterate(fn {a, b} -> {b, a + b} end)
    |> Stream.map(&elem(&1, 0))
  end
end
# import Fibonacci, only: [fib_seq: 0]
iex(2)> Enum.take(fib_seq, 10)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
iex(3)> fib_seq |> Stream.drop(100) |> Enum.take(3)
[354224848179261915075, 573147844013817084101,
  927372692193078999176]

FP Matsuri official online store T-shirts


3-2. 🍷 関数


functional programming concept map (patterns)


functional programming concept map (patterns - function)


  • 式を評価すると値が得られる

  • 部分式を関数として抽出(分解)し関数を組み合わせ(合成)て式を構成する

→ 関数が簡潔に楽に組み合わせられると旨い😋


部分適用

{- Haskell -}
λ> :t map  -- 関数はカリー(1引数関数の連鎖)化されている
map :: (a -> b) -> [a] -> [b]
λ> :t (+)  -- 演算子もカリー化されている
(+) :: Num a => a -> a -> a
λ> :t (+ 1)  -- \x -> x + 1 と等価(セクション記法による部分適用)
(+ 1) :: Num a => a -> a
λ> :t map (+ 1)  -- \xs -> map (+ 1) xs と等価(部分適用)
map (+ 1) :: Num b => [b] -> [b]
λ> map (+ 1) [0..9]
[1,2,3,4,5,6,7,8,9,10]
it :: (Num b, Enum b) => [b]
λ> f x y z = x * y * z
f :: Num a => a -> a -> a -> a
λ> :t f 2 3  -- 2番目の引数まで部分適用
f 2 3 :: Num a => a -> a
λ> f 2 3 4
24
it :: Num a => a

/* Scala */
scala> :t (1 to 10).map  // メソッドが関数になる(eta-expansion)
(Int => Any) => IndexedSeq[Any]
scala> :t (_: Int) + (_: Int)  // (x, y) => x + y と等価
(Int, Int) => Int
scala> :t (_: Int) + 1  // x => x + 1 と等価(部分適用)
Int => Int
scala> (0 to 9).map(_ + 1)
val res0: IndexedSeq[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9,
  10)
scala> def f(x: Int)(y: Int)(z: Int): Int = x * y * z
def f(x: Int)(y: Int)(z: Int): Int
scala> f(2)(3)  // 2番目の引数まで部分適用
val res1: Int => Int = Lambda/0x00001ff001554c40@478c84aa
scala> f(2)(3)(4)
val res2: Int = 24

;;; Clojure
user=> #(+ %1 %2 %3)  ; (fn [x y z] (+ x y z)) と等価
#object[user$eval245$fn__246 0x7103ab0 "user$eval245$fn__246@
7103ab0"]
user=> #(+ % 1)  ; (fn [x] (+ x 1)) と等価(部分適用)
#object[user$eval235$fn__236 0x4c03a37 "user$eval235$fn__236@
4c03a37"]
user=> (map #(+ % 1) (range 0 (inc 9)))
(1 2 3 4 5 6 7 8 9 10)
user=> (defn f [x y z] (* x y z))
#'user/f
user=> (partial f 2 3)  ; 2番目の引数まで部分適用 = #(f 2 3 %)
#object[clojure.core$partial$fn__5929 0x4ba89729 "clojure.cor
e$partial$fn__5929@4ba89729"]
user=> ((partial f 2 3) 4)
24
user=> (f 2 3 4)
24

## Elixir
iex(1)> &(&1 + &2)  # fn (x, y) -> x + y end と等価
&:erlang.+/2
iex(2)> &(&1 + 1)  # fn x -> x + 1 end と等価(部分適用)
#Function<42.81571850/1 in :erl_eval.expr/6>
iex(3)> Enum.map(0..9, &(&1 + 1))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
iex(4)> f = fn x -> fn y -> fn z -> x * y * z end end end
#Function<42.81571850/1 in :erl_eval.expr/6>
iex(5)> f.(2).(3)  # 2番目の引数まで部分適用
#Function<42.81571850/1 in :erl_eval.expr/6>
iex(6)> f.(2).(3).(4)
24

関数合成

{- Haskell: import Fibonacci (fibSeq) -}
λ> sumOfEvenFibs upper = sum (takeWhile (<= upper) (filter
  even (drop 2 fibSeq)))
sumOfEvenFibs :: Integral a => a -> a
λ> :{
λ| sumOfEvenFibs' upper = sum
λ|     $ takeWhile (<= upper)  -- f $ x = f x
λ|     $ filter even           --   (関数適用演算子)
λ|     $ drop 2 fibSeq
λ| :}
sumOfEvenFibs' :: Integral a => a -> a
λ> sumOfEvenFibs' 4000000
4613732
it :: Integral a => a

ref. #2 Even Fibonacci Numbers - Project Euler


{- Haskell -}
λ> :{
λ| sumOfEvenNums :: Integral a => a -> [a] -> a
λ| sumOfEvenNums upper = sum
λ|     . takeWhile (<= upper)  -- f . g = \x -> f (g x)
λ|     . filter even           --   (関数合成演算子)
λ| :}
sumOfEvenNums :: Integral a => a -> [a] -> a
λ> sumOfEvenNums 4000000 $ drop 2 fibSeq
4613732
it :: Integral a => a

/* Scala: import Fibonacci.fibSeq */
scala> def sumOfEvenFibs(upper: BigInt) =
     |   fibSeq
     |     .drop(2)
     |     .filter(_ % 2 == 0)
     |     .takeWhile(_ <= upper)
     |     .sum
     |
def sumOfEvenFibs(upper: BigInt): BigInt
scala> sumOfEvenFibs(4000000)
val res0: BigInt = 4613732

/* Scala */
scala> extension (nums: Seq[BigInt])  // 拡張メソッドの定義
     |   def sumOfEvenNums(upper: BigInt): BigInt =
     |     nums
     |       .filter(_ % 2 == 0)
     |       .takeWhile(_ <= upper)
     |       .sum
     |
def sumOfEvenNums(ns: Seq[BigInt])(upper: BigInt): BigInt
scala> fibSeq.drop(2).sumOfEvenNums(4000000)
val res1: BigInt = 4613732

/* Scala */
scala> def sumOfEvenNums2(upper: BigInt)(nums: Seq[BigInt]):
         BigInt =
     |   nums
     |     .filter(_ % 2 == 0)
     |     .takeWhile(_ <= upper)
     |     .sum
     |
def sumOfEvenNums2(upper: BigInt)(nums: Seq[BigInt]): BigInt
scala> import scala.util.chaining._  // pipeメソッドを導入
scala> fibSeq.drop(2).pipe(sumOfEvenNums2(4000000))
val res2: BigInt = 4613732

;;; Clojure: (require '[fibonacci :refer [fib-seq]])
user=> (defn sum-of-even-fibs [upper]
         (apply +
                (take-while #(<= % upper)
                                                    (filter even?
                                                            (drop 2 (fib-seq))))))
#'user/sum-of-even-fibs
user=> (defn sum-of-even-fibs' [upper]
         (->> (fib-seq)  ; (->> x f g) = (g (f x))
              (drop 2)   ;   (thread-lastマクロ)
              (filter even?)
              (take-while #(<= % upper))
              (apply +)))
#'user/sum-of-even-fibs'
user=> (sum-of-even-fibs' 4000000)
4613732

;;; Clojure
user=> (defn sum-of-even-fibs'' [upper]
         (transduce
          ;; 関数合成関数compでトランスデューサー(transducer)を合成
          (comp (drop 2)
                (filter even?)
                (take-while #(<= % upper)))
          +
          (fib-seq)))
#'user/sum-of-even-fibs''
user=> user=> (sum-of-even-fibs'' 4000000)
4613732
user=> (defn sum-of-even-nums [upper nums]
         (transduce (comp (filter even?)
                                                  (take-while #(<= % upper)))
                    +
                    nums))
#'user/sum-of-even-nums
user=> (->> (fib-seq) (drop 2) (sum-of-even-nums 4000000))
4613732

## Elixir: import Fibonacci, only: [fib_seq: 0]
iex(2)> require Integer
Integer
iex(3)> sum_of_even_fibs = fn upper ->
...(3)>   Enum.sum(Stream.take_while(Stream.filter(
  Stream.drop(fib_seq, 2), &Integer.is_even/1),
  &(&1 <= upper)))
...(3)> end
#Function<42.81571850/1 in :erl_eval.expr/6>
iex(4)> sum_of_even_fibs2 = fn upper ->
...(4)>   fib_seq            # x |> f |> g = g(f(x))
...(4)>   |> Stream.drop(2)  #   (パイプ演算子)
...(4)>   |> Stream.filter(&Integer.is_even/1)
...(4)>   |> Stream.take_while(&(&1 <= upper))
...(4)>   |> Enum.sum()
...(4)> end
#Function<42.81571850/1 in :erl_eval.expr/6>
iex(5)> sum_of_even_fibs2.(4000000)
4613732

## Elixir
iex(6)> sum_of_even_nums = fn nums, upper ->
...(6)>   nums
...(6)>   |> Stream.filter(&Integer.is_even/1)
...(6)>   |> Stream.take_while(&(&1 <= upper))
...(6)>   |> Enum.sum()
...(6)> end
#Function<41.81571850/2 in :erl_eval.expr/6>
iex(7)> fib_seq |> Stream.drop(2) |>
  sum_of_even_nums.(4000000)
4613732

3-3. 🍷 データ


functional programming concept map (patterns)


functional programming concept map (patterns - data)


  • 不変なデータ構造を関数を介して変換する

  • データ型を定義したり構築したり分解したり

→ 不変データ構造が標準で豊富で、データ型の定義/利用が楽だと旨い😋


標準の不変/永続コレクション

{- Haskell -}
λ> :t [1, 2, 3]  -- (連結)リスト
[1, 2, 3] :: Num a => [a]
λ> 0 : [1, 2, 3]  -- 先頭に要素追加(cons)
[0,1,2,3]
it :: Num a => [a]
λ> import qualified Data.Vector as V
λ> :t V.fromList [1, 2, 3]  -- ベクター(可変長配列)
V.fromList [1, 2, 3] :: Num a => V.Vector a
λ> V.snoc (V.fromList [1, 2, 3]) 4  -- 末尾に要素追加(snoc)
[1,2,3,4]
it :: Num a => V.Vector a

{- Haskell -}
λ> import qualified Data.Set as S
λ> :t S.fromList [1, 2, 3]  -- セット
S.fromList [1, 2, 3] :: (Ord a, Num a) => S.Set a
λ> S.insert 4 $ S.fromList [1, 2, 3]
fromList [1,2,3,4]
it :: (Ord a, Num a) => S.Set a
λ> import qualified Data.Map as M
λ> :t M.fromList [("a", 1), ("b", 2), ("c", 3)]  -- マップ
M.fromList [("a", 1), ("b", 2), ("c", 3)]
  :: Num a => M.Map String a
λ> M.insert "d" 4 $ M.fromList [("a", 1), ("b", 2), ("c", 3)]
fromList [("a",1),("b",2),("c",3),("d",4)]
it :: Num a => M.Map String a

{- Haskell -}
λ> [x * y | x <- [1..2], y <- [1..9]]  -- リスト内包表記
[1,2,3,4,5,6,7,8,9,2,4,6,8,10,12,14,16,18]
it :: (Enum a, Num a) => [a]
λ> :{
λ| do  -- do記法(a.k.a. モナド内包表記)
λ|     x <- [1..2]
λ|     y <- [1..9]
λ|     return $ x * y
λ| :}
[1,2,3,4,5,6,7,8,9,2,4,6,8,10,12,14,16,18]
it :: (Num b, Enum b) => [b]
λ> :{
λ| do  -- Maybe, Eitherなど任意のモナドで利用できる
λ|     x <- Just 2
λ|     y <- Just 3
λ|     return $ x * y
λ| :}
Just 6
it :: Num b => Maybe b

/* Scala */
scala> List(1, 2, 3)  // (連結)リスト
val res0: List[Int] = List(1, 2, 3)
scala> 0 :: List(1, 2, 3)  // 先頭に要素追加(cons)
val res1: List[Int] = List(0, 1, 2, 3)
scala> Vector(1, 2, 3)  // ベクター(可変長配列)
val res2: Vector[Int] = Vector(1, 2, 3)
scala> Vector(1, 2, 3) :+ 4  // 末尾に要素追加
val res3: Vector[Int] = Vector(1, 2, 3, 4)

/* Scala */
scala> Set(1, 2, 3)  // セット
val res4: Set[Int] = Set(1, 2, 3)
scala> Set(1, 2, 3) + 4  // 要素追加
val res5: Set[Int] = Set(1, 2, 3, 4)
scala> Map("a" -> 1, "b" -> 2, "c" -> 3)  // マップ
val res6: Map[String, Int] = Map(a -> 1, b -> 2, c -> 3)
scala> Map("a" -> 1, "b" -> 2, "c" -> 3) + ("d" -> 4)
val res7: Map[String, Int] = Map(a -> 1, b -> 2, c -> 3, d ->
  4)

/* Scala */
scala> for  // for式(a.k.a. for内包表記)
     |   x <- 1 to 2
     |   y <- 1 to 9
     | yield x * y
val res8: IndexedSeq[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9,
  2, 4, 6, 8, 10, 12, 14, 16, 18)
scala> for  // Option, EitherなどflatMap/mapを備えた型で利用できる
     |   x <- Some(2)
     |   y <- Some(3)
     | yield x * y
val res9: Option[Int] = Some(6)

;;; Clojure
user=> (type '(1 2 3))  ; (連結)リスト
clojure.lang.PersistentList
user=> (conj '(1 2 3) 0)  ; 先頭に要素追加
(0 1 2 3)
user=> (cons 0 '(1 2 3))  ; シーケンスとして要素追加
(0 1 2 3)
user=> (type [1 2 3])  ; ベクター(可変長配列)
clojure.lang.PersistentVector
user=> (conj [1 2 3] 4)  ; 末尾に要素追加
[1 2 3 4]
user=> (cons 0 [1 2 3])  ; シーケンスとして要素追加
(0 1 2 3)

;;; Clojure
user=> (type #{1 2 3})  ; セット
clojure.lang.PersistentHashSet
user=> (conj #{1 2 3} 4)  ; 要素追加
#{1 4 3 2}
user=> (cons 0 #{1 2 3})  ; シーケンスとして要素追加
(0 1 3 2)
user=> (type {:a 1 :b 2 :c 3})  ; マップ
clojure.lang.PersistentArrayMap
user=> (assoc {:a 1 :b 2 :c 3} :d 4)  ; エントリー追加
{:a 1, :b 2, :c 3, :d 4}
user=> (cons [:z 0] {:a 1 :b 2 :c 3})  ; シーケンスとして要素追加
([:z 0] [:a 1] [:b 2] [:c 3])

;;; Clojure
user=> (for [x (range 1 (inc 2))  ; forマクロ
             y (range 1 (inc 9))] ;   (a.k.a. シーケンス内包表記)
         (* x y))
(1 2 3 4 5 6 7 8 9 2 4 6 8 10 12 14 16 18)
;; 様々なデータがシーケンス(論理的なリスト)として扱える(seqable)
user=> (take 2 "abc")  ; 文字列
(\a \b)  ; 文字シーケンス
user=> (take 2 (java.util.List/of 1 2 3))  ; Javaのリスト
(1 2)  ; Clojureのシーケンス
user=> (take 2 {:a 1 :b 2 :c 3})  ; マップ
([:a 1] [:b 2])  ; エントリー(ベクター)シーケンス
user=> (require '[clojure.java.io :as io])
nil
user=> (with-open [r (io/reader "fibonacci.clj")]  ; ファイル
         (->> r line-seq (take 3) doall))
("(ns fibonacci)" "" "(defn fib [n]")  ; テキスト行シーケンス

## Elixir
iex(1)> i [1, 2, 3]  # (連結)リスト
Term
  [1, 2, 3]
Data type
  List
...
iex(2)> [0 | [1, 2, 3]]  # 先頭に要素追加(cons)
[0, 1, 2, 3]
iex(3)> i [a: 1, b: 2, c: 3]  # キーワードリスト
Term
  [a: 1, b: 2, c: 3]
Data type
  List
...
iex(4)> [{:z, 0} | [a: 1, b: 2, c: 3]]  # 先頭に要素追加
[z: 0, a: 1, b: 2, c: 3]

## Elixir
iex(5)> i MapSet.new([1, 2, 3])  # セット
Term
  MapSet.new([1, 2, 3])
Data type
  MapSet
...
iex(6)> MapSet.put(MapSet.new([1, 2, 3]), 4)
MapSet.new([1, 2, 3, 4])
iex(7)> i %{a: 1, b: 2, c: 3}  # マップ
Term
  %{c: 3, b: 2, a: 1}
Data type
  Map
...
iex(8)> Map.put(%{a: 1, b: 2, c: 3}, :d, 4)
%{c: 3, b: 2, a: 1, d: 4}

## Elixir
iex(9)> for x <- 1..2,  # Enumerableに対する内包表記
...(9)>     y <- 1..9 do
...(9)>   x * y
...(9)> end
[1, 2, 3, 4, 5, 6, 7, 8, 9, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# EnumerableであればEnumモジュールの関数で扱える
iex(10)> Enum.take([a: 1, b: 2, c: 3], 2)
[a: 1, b: 2]
iex(11)> Enum.take(MapSet.new([1, 2, 3]), 2)
[1, 2]
iex(12)> Enum.take(%{a: 1, b: 2, c: 3}, 2)
[c: 3, b: 2]
iex(13)> Enum.take([1, 2, 3], 2)
[1, 2]

データ型の定義と値の構築・分解/分岐

{- Haskell -}
λ> :{
λ| data Tree a  -- 代数的データ型の定義
λ|     = Leaf !a
λ|     | Branch { left :: !(Tree a)
λ|              , right :: !(Tree a)
λ|              }
λ|     deriving (Show, Eq)
λ| :}
type Tree :: * -> *
data Tree a = ...
left :: Tree a -> Tree a
right :: Tree a -> Tree a

{- Haskell -}
λ> t = Branch (Branch (Leaf 1) (Branch (Leaf 2) (Leaf 3)))
  (Leaf 4)
t :: Num a => Tree a
[Prelude]
λ> left t
Branch {left = Leaf 1, right = Branch {left = Leaf 2, right =
  Leaf 3}}
it :: Num a => Tree a
λ> right t
Leaf 4
it :: Num a => Tree a
λ> :{
λ| size :: Tree a -> Int
λ| size (Leaf _) = 1  -- 関数の引数でのパターンマッチ
λ| size (Branch l r) = 1 + size l + size r
λ| :}
size :: Tree a -> Int
λ> size t
7
it :: Int

/* Scala */
scala> enum Tree[+A]:  // 代数的データ型の定義
     |   case Leaf(value: A)
     |   case Branch(left: Tree[A], right: Tree[A])
     |
// defined class Tree
scala> import Tree.*
scala> val t: Branch[Int] = Branch(Branch(Leaf(1), Branch(
  Leaf(2), Leaf(3))), Leaf(4))
val t: Tree.Branch[Int] = Branch(Branch(Leaf(1),Branch(
  Leaf(2),Leaf(3))),Leaf(4))
scala> t.left
val res0: Tree[Int] = Branch(Leaf(1),Branch(Leaf(2),Leaf(3)))
scala> t.right
val res1: Tree[Int] = Leaf(4)

/* Scala */
scala> extension [A](tree: Tree[A])  // 拡張メソッドの定義
     |   def size: Int = tree match
     |     case Leaf(_) => 1
     |     case Branch(l, r) => 1 + l.size + r.size
     |
def size[A](tree: Tree[A]): Int
scala> t.size
val res2: Int = 7

;;; Clojure
user=> (defrecord Leaf [value])  ; レコードの定義
user.Leaf
user=> (defrecord Branch [left right])
user.Branch
user=> (def t
         (->Branch (->Branch (->Leaf 1)
                                                     (->Branch (->Leaf 2)
                                                               (->Leaf 3)))
                   (->Leaf 4)))
#'user/t
user=> (:left t)
#user.Branch{:left #user.Leaf{:value 1}, :right #user.Branch{
  :left #user.Leaf{:value 2}, :right #user.Leaf{:value 3}}}
user=> (:right t)
#user.Leaf{:value 4}

;;; Clojure
user=> (defmulti size class)  ; マルチメソッドの定義
#'user/size
user=> (defmethod size Leaf [_] 1)
#object[clojure.lang.MultiFn 0x6fc3e1a4 "clojure.lang.MultiFn
@6fc3e1a4"]                   ; ↓ 関数の引数での分配束縛
user=> (defmethod size Branch [{:keys [left right]}]
         (+ 1 (size left) (size right)))
#object[clojure.lang.MultiFn 0x6fc3e1a4 "clojure.lang.MultiFn
@6fc3e1a4"]
user=> (size t)
7

## Elixir
iex(1)> defmodule Leaf do  # 構造体の定義
...(1)>   defstruct [:value]
...(1)> end
{:module, Leaf, ...}
iex(2)> defmodule Branch do
...(2)>   defstruct [:left, :right]
...(2)> end
{:module, Branch, ...}
iex(3)> t = %Branch{left: %Branch{left: %Leaf{value: 1},
  right: %Branch{left: %Leaf{value: 2}, right: %Leaf{value:
  3}}}, right: %Leaf{value: 4}}
%Branch{...}
iex(4)> t.left
%Branch{left: %Leaf{value: 1}, right: %Branch{left: %Leaf{
  value: 2}, right: %Leaf{value: 3}}}
iex(5)> t.right
%Leaf{value: 4}

## Elixir
iex(6)> defmodule Tree do
...(6)>   def size(%Leaf{}), do: 1  # 関数の引数でのパターンマッチ
...(6)>   def size(%Branch{left: left, right: right}) do
...(6)>     1 + size(left) + size(right)
...(6)>   end
...(6)> end
{:module, Tree, ...}
iex(7)> Tree.size(t)
7

3-4. 🍷 評価


functional programming concept map (patterns)


functional programming concept map (patterns - lazy evaluation)


  • 純粋関数、不変データを前提とすると、評価の順序やタイミングの自由度が高まる

  • 必要になるまで評価を先送りしたり(遅延評価)、評価済みの値を再利用したり(メモ化)

→ 目的に合わせて評価を制御できると旨い😋


評価の制御と遅延コレクション

{- Haskell: 言語のデフォルトの評価戦略は遅延(非正格)評価 -}
-- 関数の定義: call by need
λ> f x y = if x > 0 then x else y
f :: (Ord a, Num a) => a -> a -> a
-- 引数yにアクセスされなければエラーが生じない
λ> f 1 (2 `div` 0)
1
it :: Integral a => a
λ> f 0 (2 `div` 0)
*** Exception: divide by zero
-- bang patternを利用した関数の定義: call by value
λ> g !x !y = if x > 0 then x else y
g :: (Ord a, Num a) => a -> a -> a
-- 引数x, yは直ちに評価される
λ> g 1 (2 `div` 0)
*** Exception: divide by zero

{- Haskell -}
-- 無限に続く整数のリストから先頭3要素を取り出す
λ> take 3 [0..]
[0,1,2]
it :: (Num a, Enum a) => [a]
-- 3番目の要素にアクセスしなければエラーが生じない
λ> [1, 2, 3 `div` 0] !! 2
*** Exception: divide by zero
λ> take 2 [1, 2, 3 `div` 0]
[1,2]

{- Haskell -}
-- データ型の定義
λ> data Pair = Pair { l :: Int, r :: Int }
...
-- r にアクセスしなければエラーが生じない
λ> l $ Pair 1 (2 `div` 0)
1
it :: Int
λ> r $ Pair 1 (2 `div` 0)
*** Exception: divide by zero
-- 値コンストラクタに正格性フラグを利用したデータ型の定義
λ> data Pair' = Pair' { l :: !Int, r :: !Int }
...
 -- r にアクセスしなくてもエラーが生じる
λ> l $ Pair' 1 (2 `div` 0)
*** Exception: divide by zero

/* Scala: 言語の評価戦略は積極(正格)評価 */
// 関数の定義: call by value
scala> def f(x: Int, y: Int): Int =
     |   if (x > 0) x else y
     |
def f(x: Int, y: Int): Int
// 引数x, yは直ちに評価される
scala> f(1, 2 / 0)
java.lang.ArithmeticException: / by zero
// 名前渡しパラメータを利用した関数の定義: call by name
scala> def g(x: => Int, y: => Int): Int =
     |   if (x > 0) x else y  // この場合、xは最大2回評価される
     |    // (回避するには評価結果をローカル束縛(メモ化)して再利用)
def g(x: => Int, y: => Int): Int
// 引数yにアクセスされなければエラーが生じない
scala> g(1, 2 / 0)
val res1: Int = 1
scala> g(0, 2 / 0)
java.lang.ArithmeticException: / by zero

/* Scala */
// 無限に続く整数の遅延リストから先頭3要素を取り出す
scala> LazyList.from(0).take(3).toList
val res3: List[Int] = List(0, 1, 2)
// 3番目の要素にアクセスしなければエラーが生じない
scala> (1 #:: 2 #:: (3 / 0) #:: LazyList.empty)(2)
java.lang.ArithmeticException: / by zero
scala> (1 #:: 2 #:: (3 / 0) #:: LazyList.empty).take(2)
  .toList
val res5: List[Int] = List(1, 2)

;;; Clojure: 言語の評価戦略は積極(正格)評価
;; 関数の定義: call by value
user=> (defn f [x y]
         (if (pos? x) x y))
#'user/f
;; 引数x, yは直ちに評価される
user=> (f 1 (/ 2 0))
Execution error (ArithmeticException) at user/eval373 ...
Divide by zero
;; マクロの定義: call by name
user=> (defmacro g [x y]
         `(if (pos? ~x) ~x ~y))  ; この場合、xは最大2回評価される
#'user/g     ; (回避するには評価結果をローカル束縛(メモ化)して再利用)
;; 引数yにアクセスされなければエラーが生じない
user=> (g 1 (/ 2 0))
1
user=> (g 0 (/ 2 0))
Execution error (ArithmeticException) at user/eval382 ...
Divide by zero

;;; Clojure
;; 無限に続く整数の遅延シーケンスから先頭3要素を取り出す
user=> (take 3 (range))
(0 1 2)
;; 3番目の要素にアクセスしなければエラーが生じない
user=> (nth (lazy-seq
             (cons 1 (lazy-seq
                      (cons 2 (lazy-seq
                                                       (cons (/ 3 0) nil))))))
            2)
Execution error (ArithmeticException) at user/eval428$fn$f...
Divide by zero
user=> (take 2 (lazy-seq
                (cons 1 (lazy-seq
                                                 (cons 2 (lazy-seq
                                                          (cons (/ 3 0) nil)))))))
(1 2)

;;; Clojure
;; 最も基本的な高階関数map, filterさえ遅延シーケンスを返す
user=> (type (map inc (range 0 (inc 9))))
clojure.lang.LazySeq
user=> (type (filter odd? (range 0 (inc 9))))
clojure.lang.LazySeq
user=> (require '[clojure.java.io :as io])
nil
;; line-seqも遅延シーケンスを返すため実体化前にリソース解放するとエラー
user=> (with-open [r (io/reader "fibonacci.clj")]
         (->> r line-seq (take 3)))
Error printing return value (IOException) at java.io.Buffered
Reader/ensureOpen (BufferedReader.java:123).
Stream closed
user=> (with-open [r (io/reader "fibonacci.clj")]
         (->> r line-seq (take 3) doall))  ; doallで直ちに実体化
("(ns fibonacci)" "" "(defn fib [n]")

## Elixir: 言語の評価戦略は積極(正格)評価
# 関数の定義: call by value
iex(1)> defmodule Some do
...(1)>   def f(x, y) do
...(1)>     if x > 0, do: x, else: y
...(1)>   end
...(1)> end
{:module, Some, ...}
# 引数x, yは直ちに評価される
iex(2)> Some.f(1, 2 / 0)
** (ArithmeticError) bad argument in arithmetic expression...

## Elixir
# マクロの定義: call by name
iex(3)> defmodule Other do
...(3)>   defmacro g(x, y) do
...(3)>     quote do
...(3)>       if unquote(x) > 0,  # この場合、xは最大2回評価される
...(3)>         do: unquote(x), else: unquote(y)
...(3)>     end
...(3)>   end # (回避するには評価結果をローカル束縛(メモ化)して再利用)
...(3)> end
{:module, Other, ...}
iex(4)> require Other
Other
# 引数yにアクセスされなければエラーが生じない
iex(5)> Other.g(1, 2 / 0)
1
iex(6)> Other.g(0, 2 / 0)
** (ArithmeticError) bad argument in arithmetic expression...

## Elixir
# 無限に続く整数のストリームから先頭3要素を取り出す
iex(7)> Enum.take(Stream.from_index, 3)
[0, 1, 2]
# 3番目の要素にアクセスしなければエラーが生じない
iex(8)> 1..3 |> Stream.map(fn n -> if n < 3, do: n, else:
  n / 0 end) |> Enum.at(2)
** (ArithmeticError) bad argument in arithmetic expression...
iex(9)> 1..3 |> Stream.map(fn n -> if n < 3, do: n, else:
  n / 0 end) |> Enum.take(2)
[1, 2]

BONUS: (評価の制御といえば)マクロ

i.e. ASTレベルでのコンパイル時メタプログラミング

;;; Clojure
(defmacro apply-after [action-fn op & args]  ; マクロの定義
  (let [args (->> args
                  (map (juxt identity pr-str))
                  (map-indexed (fn [i [expr s]]
                                                         `(doto ~expr
                                                            (~action-fn ~i ~s)))))]
    `(~op ~@args)))
  • (op arg0 arg1 ...) の個々の引数 arg0, arg1, ...がオペレータ op に適用される前に関数 action-fn の呼び出しを差し込めるマクロ

;;; Clojure
user=> (defn inspect [v i s]
         (println (str "arg" i ":") s "=>" v))
#'user/inspect
user=> (apply-after inspect + (- 1 2 3) (* 4 5) (/ 6 7))
arg0: (- 1 2 3) => -4
arg1: (* 4 5) => 20
arg2: (/ 6 7) => 6/7
118/7  ; (+ (- 1 2 3) (* 4 5) (/ 6 7)) と等価
user=> (apply-after inspect if (odd? 2)
         (println :odd)
         (println :even))
arg0: (odd? 2) => false
:even  ; (println :even) による標準出力
arg2: (println :even) => nil
nil  ; (if (odd? 2) (println :odd) (println :even)) と等価

;;; Clojure
user=> (macroexpand-1  ; マクロ展開(1段階)
        '(apply-after inspect if (odd? 2)
           (println :odd)
           (println :even)))
(if (clojure.core/doto (odd? 2)
      (inspect 0 "(odd? 2)"))
  (clojure.core/doto (println :odd)
    (inspect 1 "(println :odd)"))
  (clojure.core/doto (println :even)
    (inspect 2 "(println :even)")))
nil

3-5. 🍷 ポリモーフィズム


functional programming concept map (patterns)


functional programming concept map (patterns - polymorphism)


  • ポリモーフィズム(多相/多態性)というと、オブジェクト指向言語での継承による仕組み(= サブタイプ多相の一例)が想起されやすいかもしれない

  • 関数型言語では異なる機構が提供されていることも多い

→ 簡潔に柔軟に関数の振る舞いをポリモーフィックにできると旨い😋


サブタイプ多相に関する仕組み

/* Scala */
scala> trait Solid:  // トレイトの定義
     |   def surfaceArea: Double
     |   def volume: Double
     |
// defined trait Solid
scala> case class Cuboid(  // トレイトSolidを実装したクラスの定義
     |   a: Double,
     |   b: Double,
     |   c: Double,
     | ) extends Solid:
     |   def surfaceArea: Double =
     |     2.0 * (a * b + b * c + c * a)
     |   def volume: Double =
     |     a * b * c

/* Scala */
scala> case class Sphere(r: Double) extends Solid:
     |   def surfaceArea: Double =
     |     4.0 * Math.PI * Math.pow(r, 2)
     |   def volume: Double =
     |     4.0 / 3.0 * Math.PI * Math.pow(r, 3)
scala> def solidProps(x: Solid): Map[String, Double] =
     |   Map(
     |     "surface area" -> x.surfaceArea,
     |     "volume" -> x.volume,
     |   )
     |
def solidProps(x: Solid): Map[String, Double]
scala> Seq(Cuboid(2, 3, 4), Sphere(2)).map(solidProps)
val res0: Seq[Map[String, Double]] = List(Map(surface area ->
  52.0, volume -> 24.0), Map(surface area ->
  50.26548245743669, volume -> 33.510321638291124))

アドホック多相に関する仕組み

{- Haskell -}
λ> :{
λ| class Solid a where  -- 型クラスの定義
λ|     surfaceArea :: a -> Double
λ|     volume :: a -> Double
λ| :}
type Solid :: Constraint
λ> :{
λ| data Cuboid = Cuboid
λ|     { a :: !Double
λ|     , b :: !Double
λ|     , c :: !Double }
λ|
λ| data Sphere = Sphere { r :: !Double }
λ| :}
type Cuboid :: *
...

{- Haskell -}
λ> :{
λ| instance Solid Cuboid where  -- 型クラスのインスタンスの定義
λ|     surfaceArea (Cuboid a b c) =
λ|         2 * (a * b + b * c + c * a)
λ|     volume (Cuboid a b c) =
λ|         a * b * c
λ|
λ| instance Solid Sphere where
λ|     surfaceArea (Sphere r) =
λ|         4 * pi * r ^ 2
λ|     volume (Sphere r) =
λ|         4 / 3 * pi * r ^ 3
λ| :}

{- Haskell -}
λ> import qualified Data.Map as M
λ> :{
λ| solidProps :: Solid a => a -> M.Map String Double
λ| solidProps x = M.fromList
λ|    [("surface area", surfaceArea x), ("volume", volume x)]
λ| :}
solidProps :: Solid a => a -> M.Map String Double
λ> solidProps $ Cuboid 2 3 4
fromList [("surface area",52.0),("volume",24.0)]
it :: M.Map String Double
λ> solidProps $ Sphere 2
fromList [("surface area",50.26548245743669),("volume",
  33.510321638291124)]
it :: M.Map String Double

/* Scala */
scala> trait Solid2[A]:  // 型クラスの定義
     |   extension (a: A)
     |     def surfaceArea: Double
     |     def volume: Double
     |
// defined trait Solid2
scala> case class Cuboid2(
     |   a: Double,
     |   b: Double,
     |   c: Double,
     | )
// defined case class Cuboid2
scala> case class Sphere2(r: Double)
// defined case class Sphere2

/* Scala */
scala> given Solid2[Cuboid2] with  // 型クラスのインスタンスの定義
     |   extension (x: Cuboid2)
     |     def surfaceArea: Double =
     |       2.0 * (x.a * x.b + x.b * x.c + x.c * x.a)
     |     def volume: Double =
     |       x.a * x.b * x.c
     | given Solid2[Sphere2] with
     |   extension (x: Sphere2)
     |     def surfaceArea: Double =
     |       4.0 * Math.PI * Math.pow(x.r, 2)
     |     def volume: Double =
     |       4.0 / 3.0 * Math.PI * Math.pow(x.r, 3)
// defined object given_Solid2_Cuboid2
// defined object given_Solid2_Sphere2

/* Scala */
scala> def solidProps[A: Solid2](x: A): Map[String, Double] =
     |   Map(
     |     "surface area" -> x.surfaceArea,
     |     "volume" -> x.volume,
     |   )
     |
def solidProps[A](x: A)(using evidence$1: Solid2[A]):
  Map[String, Double]
scala> solidProps(Cuboid2(2, 3, 4))
val res0: Map[String, Double] = Map(surface area -> 52.0,
  volume -> 24.0)
scala> solidProps(Sphere2(2))
val res1: Map[String, Double] = Map(surface area ->
  50.26548245743669, volume -> 33.510321638291124)

;;; Clojure
user=> (defprotocol Solid  ; プロトコルの定義
         (surface-area [this])
         (volume [this]))
Solid
user=> (defrecord Cuboid [a b c])
user.Cuboid
user=> (defrecord Sphere [r])
user.Sphere

;;; Clojure
user=> (extend-protocol Solid  ; プロトコルの実装
         Cuboid
         (surface-area [{:keys [a b c]}]
           (* 2 (+ (* a b)
                   (* b c)
                   (* c a))))
         (volume [{:keys [a b c]}]
           (* a b c))
         Sphere
         (surface-area [{:keys [r]}]
           (* 4 Math/PI (Math/pow r 2)))
         (volume [{:keys [r]}]
           (* 4/3 Math/PI (Math/pow r 3))))
nil

;;; Clojure
user=> (defn solid-props [x]
         {:surface-area (surface-area x)
          :volume (volume x)})
#'user/solid-props
user=> (solid-props (->Cuboid 2 3 4))
{:surface-area 52, :volume 24}
user=> (solid-props (->Sphere 2))
{:surface-area 50.26548245743669, :volume 33.51032163829112}

## Elixir
iex(1)> defprotocol Solid do  # プロトコルの定義
...(1)>   def surface_area(x)
...(1)>   def volume(x)
...(1)> end
{:module, Solid, ...}
iex(2)> defmodule Cuboid do
...(2)>   defstruct [:a, :b, :c]
...(2)> end
{:module, Cuboid, ...}
iex(3)> defmodule Sphere do
...(3)>   defstruct [:r]
...(3)> end
{:module, Sphere, ...}

## Elixir
iex(4)> defimpl Solid, for: Cuboid do  # プロトコルの実装
...(4)>   def surface_area(%Cuboid{a: a, b: b, c: c}) do
...(4)>     2 * (a * b + b * c + c * a)
...(4)>   end
...(4)>   def volume(%Cuboid{a: a, b: b, c: c}) do
...(4)>     a * b * c
...(4)>   end
...(4)> end
{:module, Solid.Cuboid, ...}
iex(5)> defimpl Solid, for: Sphere do
...(5)>   def surface_area(%Sphere{r: r}) do
...(5)>     4 * :math.pi() * r ** 2
...(5)>   end
...(5)>   def volume(%Sphere{r: r}) do
...(5)>     4 / 3 * :math.pi() * r ** 3
...(5)>   end
...(5)> end
{:module, Solid.Sphere, ...}

## Elixir
iex(6)> solid_props = fn x ->
...(6)>   %{
...(6)>     surface_area: Solid.surface_area(x),
...(6)>     volume: Solid.volume(x)
...(6)>   }
...(6)> end
#Function<42.81571850/1 in :erl_eval.expr/6>
iex(7)> solid_props.(%Cuboid{a: 2, b: 3, c: 4})
%{surface_area: 52, volume: 24}
iex(8)> solid_props.(%Sphere{r: 2})
%{
  surface_area: 50.26548245743669,
  volume: 33.510321638291124
}

パラメータ多相に関する仕組み

{- Haskell -}
λ> :t id  -- 関数idの型(type)は?
id :: a -> a  -- 型a
λ> id 42  -- idを数値に適用したとき
42
it :: Num a => a  -- 型クラスNumの制約付きの型a
λ> :t Just  -- 1引数の値コンストラクタJust
Just :: a -> Maybe a
λ> :t Just 42  -- Justを数値に適用したとき
Just 42 :: Num a => Maybe a
λ> :t Nothing  -- 0引数の値コンストラクタNothing
Nothing :: Maybe a
-- cf. 型Maybe aの定義(抜粋)
data Maybe a = Nothing | Just a

{- Haskell -}
λ> :k Maybe  -- 型コンストラクタMaybeのカインド(kind)は?
Maybe :: * -> *  -- 型レベルでの関数に相当
λ> :k Num a => Maybe a  -- Maybeを型aに適用したとき
Num a => Maybe a :: *  -- 型レベルでの値に相当
λ> :k Functor  -- 型クラスFunctor
Functor :: (* -> *) -> Constraint  -- 型レベルでの高階関数に相当
λ> :k Functor Maybe  -- FunctorをMaybeに適用したとき
Functor Maybe :: Constraint
-- cf. 型クラスFunctorの定義(抜粋)
class Functor f where
    fmap :: (a -> b) -> f a -> f b

型(type) カインド(kind)
値(value):
a
型(type):
*
関数(function)/値コンストラクタ(value constructor):
a -> b
型コンストラクタ(type constructor):
* -> *
高階関数(higher-order function):
(a -> b) -> f a -> f b
高カインド型(higher-kinded type):
(* -> *) -> *

/* Scala */
scala> [A] => (x: A) => identity(x)
val res0: [A] => (x: A) => A = Lambda/0x00002000015018d8@3...
scala> identity(42)
val res1: Int = 42
scala> [A] => (x: A) => Some(x)
val res2: [A] => (x: A) => Some[A] = Lambda/0x000020000150...
scala> val x: Option[Int] = Some(42)  // SomeはOptionの派生型
val x: Option[Int] = Some(42)
scala> val y: Option[Int] = None  // NoneはOptionの派生型
val y: Option[Int] = None
scala> val z: Option[Any] = x  // 型パラメータAは共変(covariant)
val z: Option[Any] = Some(42)
// cf. 関数identityの定義(抜粋)
def identity[A](x: A): A = x
// cf. 型Option[+A]の定義(抜粋)
sealed abstract class Option[+A] extends IterableOnce[A]
  with Product with Serializable
final case class Some[+A](value: A) extends Option[A]
case object None extends Option[Nothing]

/* Scala */
// 型クラスFunctorの定義
scala> trait Functor[F[_]]:
     |   extension [A](x: F[A])
     |     def fmap[B](f: A => B): F[B]
     |
// defined trait Functor
// Optionに対するインスタンスの定義
scala> given Functor[Option] with
     |   extension [A](x: Option[A])
     |     def fmap[B](f: A => B): Option[B] =
     |       x.map(f)
     |
// defined object given_Functor_Option
scala> (x, y)
val res3: (Option[Int], Option[Int]) = (Some(42),None)
scala> x.fmap(_ + 1)
val res4: Option[Int] = Some(43)
scala> y.fmap(_ + 1)
val res5: Option[Int] = None

4. 関数型プログラマの

メンタルモデル


functional programming concept map (values)


関数型言語使い/関数型プログラミング実践者の発想

  • ⛓️ 適切な制約が解放をもたらす
    • 引数に対して戻り値を返す以外のことをする関数は信頼できない😱 → 純粋関数を基本に
    • いつでもどこでも破壊的に更新できるデータ構造は怖い😱 → 不変/永続データを基本に
    • とりうる値が分からないのは不安😱 → 不正値を表現不能に、より(型)安全に
  • 🧱 単純で安定したブロックを基礎に全体を構成したい
    • → 式指向に、宣言的に、合成可能に

おわりに

  • Haskell, Scala, Clojure, Elixirを例に、関数型言語で楽しめる基本的な旨みについて探ってみた

  • 関数型プログラミングの本質、そこから導かれる不可欠な要素について考えるきっかけになれば幸い

関数型プログラミング(言語)と「関数型まつり」

引き続きぜひご賞味ください🍷


Further Reading

言語横断


Haskell


Scala


Clojure


Elixir & Erlang

Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View raw

(Sorry about that, but we can’t show files that are this big right now.)

Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View raw

(Sorry about that, but we can’t show files that are this big right now.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment