- 株式会社スマートラウンドのシニアエンジニア
- スタートアップの起業家と投資家のための業務効率化/連携プラットフォームを開発している
- 主要技術スタック: Kotlin & TypeScript
- Server-Side Kotlin Meetupの運営にも協力
- 関数型プログラミング(言語)とLispの熱烈な愛好者
- 関数型まつり2025の運営スタッフ(座長のひとり)
- 関数型言語経験: 約12年
-
cf. プログラミング経験: 約13年
-
プログラミング学習初期から関数型言語と戯れてきた(そして非関数型言語に未だに馴染めない😇)
-
仕事で: Clojure, Scala
-
趣味で: Clojure, Haskell, Erlang/Elixir, etc.
-
- 初期(2013年〜)の学びの主な機会
-
関数型プログラミング(言語)に関する技術書/記事
-
コミュニティイベント
- 勉強会: Tamachi.clj, Shibuya.lisp, 渋谷java, tokyo.ex, rpscala, etc.
- もくもく会: Haskellもくもく会, Laketown.clj
- 読書会: 関数型プログラミング勉強会(Scala), SICP読書会
- カンファレンス: Haskell Day, ScalaMatsuri, Erlang & Elixir Fest
-
オーム社の書籍詳細ページより
『すごいHaskellたのしく学ぼう!』(すごいH本)
オーム社の書籍詳細ページよりオーム社の書籍詳細ページより
オーム社の書籍詳細ページより
『すごいErlangゆかいに学ぼう!』(すごいE本)
達人出版会の書籍詳細ページより『Scala関数型デザイン&プログラミング』(FP in Scala)
インプレスブックスの書籍詳細ページよりStructure and Interpretation of Computer Programs (SICP, 魔術師本)
Wikipedia英語版のStructure and Interpretation of Computer Programsより-
きっかけ
-
「関数型プログラミング」
-
🍷 テイスティング
- 4つの関数型言語の基本
- 関数
- データ
- 評価
- ポリモーフィズム
-
関数型プログラマのメンタルモデル
関数型プログラミング(言語)に関する解説で
🐬は以下のような表現をよく目にする:
近年、関数型プログラミング(言語)のエッセンスが多くの主流言語に取り入れられてきました。
関数型プログラミング(言語)の
「エッセンス」??? 🤔
- 語源: 🇬🇧
essence
<essentia
<esse
+-ia
(≒ 🇬🇧 beingness, あるということ) - 語義: 「本質(的に重要なもの)」
- 抽出物(エキス)という派生的/比喩的な意味も
- 本来、単なる成分/要素のような意味ではない
- cf. 🇫🇷 哲学の文脈で関連する表現
- l'existence précède l'essence (実存は本質に先立つ)
- L'essentiel est invisible pour les yeux. (🦊「大切なものは目に見えないんだよ。」)
ワイン🍷テイスティングでは色(視覚)・香り(嗅覚)・味(味覚)から吟味するように、
4つの関数型言語を例にその味わいを多角的に探り、「エッセンス」(= 本質)を見出したい
-
関数型プログラミング := 純粋関数を基本要素としてその組み合わせによってプログラムを構成していくプログラミングスタイル
- → 言語を問わず実践可能(実践しやすさは異なる)
-
関数型言語 := 関数型プログラミングが言語/標準ライブラリレベルで十分に支援される(そして関数型プログラミングスタイルがユビキタスな)言語
- → 例えばJavaScript/TypeScriptやJava、Kotlin、古典的なLisp方言は含めない
※ 🐬が思い浮かぶ概念/用語を連想的に列挙したもの(網羅的でも体系的でもない)
- 4つの関数型言語の基本
- 関数
- データ
- 評価
- ポリモーフィズム
Haskell, Scala, Clojure, Elixir
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]
{- 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
{- 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]
[PR] 関数型まつり 公式オンラインストアのグッズ👕
-
式を評価すると値が得られる
-
部分式を関数として抽出(分解)し関数を組み合わせ(合成)て式を構成する
→ 関数が簡潔に楽に組み合わせられると旨い😋
{- 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
-
不変なデータ構造を関数を介して変換する
-
データ型を定義したり構築したり分解したり
→ 不変データ構造が標準で豊富で、データ型の定義/利用が楽だと旨い😋
{- 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
-
純粋関数、不変データを前提とすると、評価の順序やタイミングの自由度が高まる
-
必要になるまで評価を先送りしたり(遅延評価)、評価済みの値を再利用したり(メモ化)
→ 目的に合わせて評価を制御できると旨い😋
{- 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]
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
-
ポリモーフィズム(多相/多態性)というと、オブジェクト指向言語での継承による仕組み(= サブタイプ多相の一例)が想起されやすいかもしれない
-
関数型言語では異なる機構が提供されていることも多い
→ 簡潔に柔軟に関数の振る舞いをポリモーフィックにできると旨い😋
/* 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
- ⛓️ 適切な制約が解放をもたらす
- 引数に対して戻り値を返す以外のことをする関数は信頼できない😱 → 純粋関数を基本に
- いつでもどこでも破壊的に更新できるデータ構造は怖い😱 → 不変/永続データを基本に
- とりうる値が分からないのは不安😱 → 不正値を表現不能に、より(型)安全に
- 🧱 単純で安定したブロックを基礎に全体を構成したい
- → 式指向に、宣言的に、合成可能に
-
Haskell, Scala, Clojure, Elixirを例に、関数型言語で楽しめる基本的な旨みについて探ってみた
-
関数型プログラミングの本質、そこから導かれる不可欠な要素について考えるきっかけになれば幸い
関数型プログラミング(言語)と「関数型まつり」を
引き続きぜひご賞味ください🍷
- 『7つの言語 7つの世界』: Scala, Erlang, Clojure, Haskell
- 続編Seven More Languages in Seven Weeks: Elm, Elixir, Idris
- 『計算機プログラムの構造と解釈 第2版』(通称: SICP, 魔術師本): サンプルコードはScheme
- 『型システム入門 プログラミング言語と型の理論』(通称: TAPL): サンプルコードはOCaml
- 『純粋関数型データ構造』(通称: PFDS): サンプルコードはStandard ML, Haskell
- 『プログラミングHaskell 第2版』
- 『すごいHaskellたのしく学ぼう!』(通称: すごいH本)
- 『[増補改訂]関数プログラミング実践入門』
- 『Haskell入門 関数型プログラミング言語の基礎と実践』
- 『なっとく!関数型プログラミング』
- 『実践Scala入門』
- 『Scala関数型デザイン&プログラミング』(通称: FP in Scala)
- 『Scalaスケーラブルプログラミング 第4版』(通称: コップ本)
- Functional Programming Patterns in Scala and Clojure
- 『プログラミングElixir(第2版)』
- 『Elixir実践入門』
- 『プログラミングErlang』(通称: 飛行機本)
- 『すごいErlangゆかいに学ぼう!』(通称: すごいE本)