読者です 読者をやめる 読者になる 読者になる

conjによるintoの実装

brave and trueでintoによってconjが実装できるという話があった。

で、逆にconjinto実装できるだろうと思ったので書いてみた。無意味な手慰みだがdestructuringに慣れるくらいの気持ちでタイピング。

(defn my-into
  [x [y & ys]]
  (if (empty? ys)
    (conj x y)
    (my-into (conj x y)
             ys)))

これで一応本に出てくる入力例は正しくパスした。coreの実装を見ると自分の知識では分からない作法が多数あるので勉強進めよう。

Clojure for the Brave and True: Chapter 03 読み

Do Things: A Clojure Crash Course | Clojure for the Brave and True

前回はchapter 01を読んだ。chapter 02についてはEmacsの話なので取り扱わない(使ってはいるが操作がおぼつかず、文章にまとまらない。気が向いたら初心者の気持ちとして感想文を書きたい)。

文法

フォーム

Clojureのコードはすべて一様な構造を持ち、それはフォーム(form)と呼ばれている。 フォームには2種類ある。

  • データ構造のリテラル表現
  • データに対する操作(operation)

以下のリテラルは全てフォーム。

1
"a string"
["a", "vector", "of", "strings"]

リテラルリテラル単体でふわっと存在するのは稀で、なんらかの操作と一体になっているのが普通である。

(operator operand1 operand2 ... operandn)

operandは被演算子という。operator(演算子)の操作を受ける何か、という意味である。operandの区切りはCファミリー言語とかではコンマ,で行うが、clojureではスペース区切りである。

(+ 1 2 3)
; => 6
(str "It was the panda " "in the library " "with a dust buster")
; => "It was the panda in the library with a dust buster"

(原文には掃除機を持ったかわいいパンダの絵があります。図書館にいるのかは分かりませんが)

上の例では+演算子1, 2, 3がそれぞれ被演算子である。足し算をする(見たまんま)。

strは3つの文字列を結合する演算子である。

上の2つの式はそれぞれ有効(valid)なフォームである。

有効でないフォームの例をあげる。

(+

カッコが閉じていないのでこれはフォームでない。

制御フロー

if

if式は次のようになっている。

(if boolean-form
  then-form
  optional-else-form)

elseに相当する部分は必ずしも書かなくてよいことがわかる。

例えば次のような式はnilを返す。

(if false
  "By Odin's Elbow!")

C言語ファミリーなどの手続き型と比べて、なんの変哲もないようだが、実はclojureのifには重要な違いが一つある。

それは、ifで分岐した後のthen-form, else-formでは、それぞれたった1つのフォームしか書けないという点である。

rubyでは次のようにif中に命令列を書くことができるが、このようなことはclojureでは許可されていない。

if true
  doer.do_thing(1)
  doer.do_thing(2)
else
  other_doer.do_thing(1)
  other_doer.do_thing(2)
end

理由は分からないが、データ構造のuniformity(統一性, 均質性)を担保するためにそうなっているのだと思う。

もっとも、この制限には解決策が用意されている。それが次のdo演算子である。

do

clojureでは、doを使って複数のフォームを1つのフォームにまとめることができる。

(if true
  (do (println "Success!")
      "By Zeus's Hammer!"))
when

when(if ... (do ..))の短縮形である。elseには分岐しない。

(when true
  (println "Success!")
  "abra cadabra")
; => Success!
; => "abra cadabra"

これもfalseの場合は値がないことを表すnilを返す。

nil, true, false, 真らしさ, そしてブール式
(nil? 1)
; => false

(nil? nil)
; => true
(nil (when false (println "Success!")))
; => true

nil, falseはどちらも偽っぽい値(falsy value)を表している。その他の値は真っぽい値(truthy value)として扱われる。

(if "bears eat beets"
  "bears beets Battlestar Galactica")
; => "bears beets Battlestar Galactica"

(if nil
  "This won't be the result because nil is falsey"
  "nil is falsey")
; => "nil is falsey"

ここで文字列"bears eat beets"は真らしい値と考えられる。

clojureの等価演算子=である

(= 1 1)
; => true

(= nil nil)
; => true

(= 1 2)
; => false

ブール演算子or, andを見ていく。

orの挙動はすこし癖がある。

(or false nil :large_I_mean_venti :why_cant_I_just_say_large)
; => :large_I_mean_venti

(or (= 0 1) (= "yes" "no"))
; => false

(or nil)
; => nil

左から被演算子を評価していき、最初の真らしい値を返す。全て偽だった場合最後の値を返す。この最後の値を返すというのがトリッキーで、値はfalseかもnilかもしれない。どちらにせよfalsyな値なのでif分岐をするには問題ない。orで評価した結果の値を持ち回りたい時に、nilfalseかに留意していないとなにかトラブルが起きそうな予感がする。

andは左から被演算子を評価していき最初の偽らしい値を返すか、全ての値が真らしいなら最後の値を返す。

(and :free_wifi :hot_coffee)
; => :hot_coffee

(and :feelin_super_cool nil false)
; => nil

defで値に名前をつける

clojureにおいて、defは名前に値を束縛するものである。

(def failed-protagonist-names
  ["Larry Potter" "Doreen the Explorer" "The Incredible Bulk"])

failed-protagonist-names
; => ["Larry Potter" "Doreen the Explorer" "The Incredible Bulk"]

束縛は他言語の概念「値の変数割り当て」とは異なる、という点に注意する。

rubyでは次のように多重代入を許可しているが、これはclojureでは基本的に許可されていない(clojureで破壊的変更を扱う方法はchapter 10で解説されているとの由)。

severity = :mild
error_message = "OH GOD! IT'S A DISASTER! WE'RE "
if severity == :mild
  error_message = error_message + "MILDLY INCONVENIENCED!"
else
  error_message = error_message + "DOOOOOOOMED!"
end

上のコードと似たようなことはclojureでは次のようにできる。再束縛をしているだけ。

(def severity :mild)
(def error-message "OH GOD! IT'S A DISASTER! WE'RE ")
(if (= severity :mild)
  (def error-message (str error-message "MILDLY INCONVENIENCED!"))
  (def error-message (str error-message "DOOOOOOOMED!")))

一つ言えることは、名前に関連づけられている値の変更が、本当に必要になるケースは少ないということだ。そして不変な束縛値を扱うことで、プログラムの挙動の理解を容易たらしめるという利点がある。

データ構造

すこしダラダラやりすぎたので飛ばしていく。一応immutableの簡単なところはわかっているつもりなので斜め読み。

Clojureのデータ構造は基本的に不変で、破壊的変更を許さない。これはコードに単純さをもたらす一方で、つらい面があるのだが、つらい面はchapter 10に譲るらしいので今は気にしない。

数値

clojureの数値のサンプルとして以下が挙げられている

93
1.2
1/5

1/5は最初(/ 1 5)の間違いかと思ったが、カッコなしの1/5で一つのフォームなのだとreplの挙動を見て納得した。例えば次のように書ける。

(+ 1/5 2/5 2/5)
; => 1N

文字列

(ハッシュ)マップ

マップリテラル{}(hash-map ...)で組み立てる。getでキーから値を参照。

get-inを使うとネストしたマップを参照できる。

(get-in {:a 0 :b {:c "ho hum"}} [:b :c])
; => "ho hum"

:aとか:bとかいうのはキーワードという。次のセクションで説明する。

また、getを使わなくてもそのまま関数ライクに参照できる。

({:name "The Human Coffeepot"} :name)
; => "The Human Coffeepot"

キーワード

clojureのキーワードはたいがいマップのキーとして使われる。

以下はすべてキーワードとして有効である。

:a
:rumplestiltsken
:34
:_?

マップの参照は右からでも左からでも行える。どういうことなの…

(:a {:a "Hello"})
; => "Hello"
({:a "Hello"} :a)
; => "Hello"

Vector

配列に似たデータ構造で、これもgetで値を取れる。

(get [3 2 1] 0)
; => 3
([3 2 1] 0)
; => 3

ちなみにvectorに入っている値の型は同じでなくてよい。

conjで最後尾に値を追加したvectorを得ることができる。

(conj [1 2 3] 4)
; => [1 2 3 4]

vectorに似たデータ構造としてリストがある。

リスト

リストはgetが使えない。getや添え字アクセスが使えない理由は、推測になるが、リストの構造が単方向リンクリストだからで、頭から値を舐めているので要素にO(1)アクセスできないからだと思う。代わりに添え字アクセスにはnthを使う。

リテラルの組み立て方はquoteを使って'(1 2 3)という感じ。

Sets(集合)

hash-setとsorted-setという2つのデータ構造があり、ここではhash-setを紹介する。

Setsは重複を許さない値の集まりを表している。リテラル#{"kurt vonnegut" 20 :incicle}という形で値を組み立てる。

演算子からsetを組み立てるにはhash-setを使う。

(hash-set 1 1 2 2)
; => #{1 2}

またsetvectorやリストをsetに変えることができる。

関数

関数の呼び出し

関数の呼び出しはカッコ()を伴う。次のようなフォームはエラーである。

(1 2 3 4)
("test" 1 2 3)

試しに動かすと次のようなエラーを得る。

1. Unhandled java.lang.ClassCastException
  java.lang.String cannot be cast to clojure.lang.IFn

「"なんとか"はclojure.lang.IFnにキャストできない」という形式のエラーである。関数でないもので何かを呼び出そうとするとこういうことになる。

高階関数については略。

マクロ呼び出し、そして特殊形式(special forms)

少し寄り道。

関数呼び出しのほかに2つの式がある。それはマクロ呼び出しと特殊形式である。 特殊形式はすでにこの記事内で使われたことがある。ifがその例である。詳しくはChapter 7で。今のところ関数と特殊形式の違いとして、関数は全ての被演算子を評価するが、特殊形式は必ずしもすべての被演算子を評価するわけではないということに注意されたい。

また特殊形式は基本的にclojureのコア機能として実装されており、関数では実装できないものである。

マクロは特殊形式と似ているが関数と違う方法で被演算子を評価する。よくわからん。

寄り道終了。関数の作り方を見ていく。

関数の定義

defnしてdocstring書いて引数はカギカッコで〜というのはどうでもいいので、arity overloadingについて見ていく。

パラメータと引数

引数の数に応じて走らせる関数の本体を切り替えることができる。これがarity overloadingである。

(defn multi-arity
  ([a]
    (println a))
  ([a b]
    (println a b))
  ([a b c]
    (println a b c)))

(multi-arity "Hello") ; => Hello
(multi-arity "Hello, " "World!") => Hello, World!
(multi-arity "Hello, " "World!" "Yay!" "Yay!") ; => ArityException Wrong number of args (4) passed to ...

可変長引数は引数の頭に&をつければよい。

分配束縛(destructuring)

コレクションである引数を簡易に名前束縛する方法である。使い方を見た方が早い。

(defn my-first
  [[first-thing & garbage]] ; Notice that first-thing is within a vector
  first-thing)

(my-first ["oven" "bike" "war-axe"])
; => "oven"

& garbageには引数の残りの["bike" "war-axe"]が入っている。

マップの分配束縛もできるが、記法がリテラルを構築する時と逆順になっている点が面白い。

(defn announce-treasure-location
  [{lat :lat lng :lng}]
  (println (str "Treasure lat: " lat))
  (println (str "Treasure lng: " lng)))

(announce-treasure-location {:lat 28.22 :lng 81.33})
; => Treasure lat: 28.22
; => Treasure lng: 81.33

キーから値を剥がすだけなら便利な記法がある。

(defn announce-treasure-location
  [{:keys [lat lng]}]
  (println (str "Treasure lat: " lat))
  (println (str "Treasure lng: " lng)))

これで前述のコードと等価なことができる。また:asを使って[{:keys [lat lng] :as treasure-location}]とすることでtreasure-locationから元の引数のマップにアクセスできるようになる。

関数の本体

フォームはいくらでも並べることができるが、返り値になるのは最後のフォーム。ただしifのような特殊形式を使った場合はこの限りでない。

匿名関数

(fn [param-list] function-body)で書ける。

また#(...)という略記法があり、引数は%, %1, %2, %&などで表される。

返り値としての関数

関数から返された関数をクロージャというらしい。クロージャは自分を作った関数の中にある変数にアクセスできる。

(defn inc-maker
  "Create a custom incrementor"
  [inc-by]
  #(+ % inc-by))

(def inc3 (inc-maker 3))

(inc3 7)
; => 10

ぜんぶひっくるめる

let, loop/recur, 正規表現の簡単な使い方の説明になっている。最後にreduceでコードを簡略化している。全体のコードは長いので引用せず雑にごちゃごちゃ言うだけにする。

let

まずlet、これはdefと違ってローカルな文脈に変数を束縛する。式本体部がごちゃごちゃしているがそこは無視してよい。

ここではremaining-asym-partsの要素を頭から1つずつ剥がすのに使っている。

loop / recur

setとかinto, matching-partで何をしているのかというと、vectorに体のパーツを詰めている。

(set [part (matching-part part)])でパーツを複製し"right-"が出てこなければ1つにまとまるようにしている。

intovectorにパーツを追加している。final-body-partsとは返り値にするために用意したvectorで、初期値は[]にセットされている((loop [...]の部分)。recurするたびに要素が第2引数に蓄積されていく。

loop/recurはちょっと学習曲線が急だと思う。それなのにここでは色々と別の要素が詰め込まれてて知らない人は混乱すると思う…)

Exercises

5と6はよくわからんです…body-partsのデータくらいは与えて欲しいんですが甘えですか…

Clojure for the Brave and True: Chapter 01 読み

かんたんなスニペットをREPLを利用して書いていく章。言語機能をひとめぐりするのにREPLは有用だ。

ところでClojureのプログラムはLeiningenというツールを使ってビルトするのが常識(de facto standard)になっているらしい。よって、ここではまずLeiningenを覚える必要がある。

本章の流れは以下のようになる。

  • LeiningenでClojureのプロジェクトを生成する
  • プロジェクトをビルドしjarを生成する
  • jarを実行する
  • Clojure REPL内でコードを実行する

Clojureとはなにか

そもそも論のようだ。

Clojure was forged in a mythic volcano by Rich Hickey. Using an alloy of Lisp, functional programming, and a lock of his own epic hair, he crafted a language that’s delightful yet powerful.

Clojureは神話の火山でリッチ・ヒッキーが鍛造しました。Lisp合金を用い、関数型プログラミングを組み合わせ、彼の英雄的な髪を一房巻きつけ、快適でありながら力強い言語を作り上げたのです。

英語圏特有のなんかカッコイイ言い回しが続くので以下略。

第2段落。Clojureの言語仕様とClojureの実装であるclojure.jarは別物と述べている。clojure.jarはClojureのコードをコンパイルしてJVM向けにバイトコードを吐き出すんですよ、という。

疑問 それでは、clojure.jar以外にclojureの実装は存在するのだろうか?

調べた結果 ClojureCLRという.Net上のCommon Language Runtimeで動くものがある(C#実装), また、 ClojureScript(ClojureJavaScriptに変換するトランスパイラ)が存在するらしい。

(上の疑問についてはすぐ下の段落で述べられていた…くそ…)

JVM向けの実装はJVMのコアフィーチャーに依存する(スレッディングやGCなど)。本書はJVM実装のみについて扱っている。

JVMClojureの関係については後ほど述べられるが、今の所必要な認識としては

Leiningen

Leiningenの役割。

  • Clojureプロジェクトの作成
  • Clojureプロジェクトの実行
  • Clojureプロジェクトのビルド
  • REPL

Java(JRE)のバージョンが古いと困ったことになるらしいが、自分は問題なしだったのでスルー。

とりあえず使ってみる。

新しいClojureプロジェクトの作成

$ lein --version
Leiningen 2.7.1 on Java 1.8.0-ea Java HotSpot(TM) 64-Bit Server VM
$ lein new app hello
$ cd hello

以下のようなディレクトリ構成になる。

- hello/
 |+ doc/
 |+ resources/  
 |- src/        
  |- hello/     
   |  core.clj  
 |+ test/       
 |  .gitignore  
 |  .hgignore   
 |  CHANGELOG.md
 |  LICENSE     
 |  README.md   
 |  project.clj 

project.cljはLeiningen用の設定ファイルで、依存関係、プロジェクトを実行するとき、どのファイルのどの関数を実行するか?(ここではsrc/hello/core.clj-main関数)などを設定している。

Clojureプロジェクトの実行

core.cljのソースは次のようになっている。

(ns hello.core
  (:gen-class))

(defn -main
  "I don't do a whole lot ... yet."
  [& args]
  (println "Hello, World!"))

ns名前空間を宣言しているが気にしなくていい。-mainはプログラムのエントリポイントである。-mainは何か特別なもので、この本ではAppendix Aで補足説明されている(らしい)。

Clojureプロジェクトのビルド

$ lein uberjar

でビルドし、スタンドアロン化することができる。スタンドアロン化とはleiningenがなくても誰のパソコンマシンでも実行できるjarファイルを作ることである(JREは必要)。jarは次のコマンドで実行できる。

$ java -jar target/uberjar/clojure-noob-0.1.0-SNAPSHOT-standalone.jar

REPL

$ lein repl

で対話的環境が起動する。プロジェクト内で起動するとsrc/hello/core.clj名前空間に入っていることがわかる。

準備

clojureの勉強を始めました。brave and trueを読みながら4clojureを解いています。4clojureは現在elementaryとeasyだけ70問ときました。brave and trueはノートを取りながらchapter 4に入ったところです。これを機に覚えたいと思っていたemacsにも手をだしました。しかし今のところ思うようにコーディングできなくて結構厳しいなあという気持ちです。

そのうち記事を書きます。