F# 5.0の新機能 その2
F# Advent Calendar 2020の1日目の記事であるF# 5.0の新機能で大体書かれているのですが、2つ抜けているものがあるので紹介します。
nameofパターン
nameof
の値でパターンマッチできます。
RFCにはタグを nameof
で取り出しておいて、それを元にデシリアライズする例が載っています。
// 使う型 type RecordedEvent = { EventType: string; Data: byte[] } type MyEvent = | A of AData | B of BData
// シリアライズ用関数 let serialize (e: MyEvent) : RecordedEvent = match e with | A adata -> { EventType = nameof A; Data = JsonSerializer.Serialize<AData>(adata) } | B bdata -> { EventType = nameof B; Data = JsonSerializer.Serialize<BData>(bdata) }
// デシリアライズ用関数 let deserialize (e: RecordedEvent) : MyEvent = match e.EventType with | nameof A -> A (JsonSerializer.Deserialize<AData>(e.Data)) | nameof B -> B (JsonSerializer.Deserialize<BData>(e.Data)) | t -> failwithf "Invalid EventType: %s" t
地味に便利ですね。
string関数の実装変更
string
は今まで静的に解決される型パラメーターを取るinline関数だったのですが、F#5.0からは普通の型パラメーターを取るinline関数になります。
また、今までデッドコードだったコードを有効化し、より多くの型に対するケースを追加しています。
これは例を見てもらうとびっくりするかもしれません。
// 今まではエラーだったが、F#5.0からはエラーにならない type Either<'L, 'R> = | Left of 'L | Right of 'R override this.ToString() = match this with | Left x -> string x | Right x -> string x
他にもフォーマットに "g"
を指定したのを null
に変更したりもしています。
diffを見てもらった方が分かりやすいかもしれません。
Optionalは引数に使うべきでない、という幻想について
継続渡しすると戻り値は引数になるから「Optional
は戻り値にのみ使うべき」というルールは無意味だよ、という話。
あ、そういう話ね、と分かった方はこれ以上読む必要はありません。
Mono
が Async
+ Optional
+ 例外という欲張りパック状態なのも問題ですが、それについてはまた今度(Mono<Optional<T>>
使わずに Mono<T>
を使え、という指摘があり得る。ただ、そっちもそっちで言いたいことはある、という程度)。
今回は、 Mono
は Async
くらいの意図として使っています*1。
まず、こんなメソッドがあったとします。
Mono<Optional<String>> f();
これ自体は戻り値に Optional
を使っているだけなので、「Optional
は戻り値にのみ使うべき」は守っています。
しかし、これを使う側はそうはいきません。 例えば、値が取ってこれなかった場合はログして404を返し、取れてきた場合はログして200を返すような処理を考えます。
Mono<ServerResponse> g(ServerRequest req) { return f().map(strOpt -> { if (strOpt.isEmpty()) { logger.info("str is not found."); return ServerResponse.notFound(); } var str = strOpt.get(); logger.info("str is {}.", str); return ServerResponse.ok(); }); }
ここで、map
に渡すラムダ式の引数は Optional<String>
になります。
おおっと、引数に Optional
が出てきてしまいました。
このように、Mono
など「後続処理をラムダ式として受け取る」ようなスタイルの設計においては、戻り値を引数として受け取ることになります。
これを「ラムダ式の引数は例外とする」というルールを加えてしまうと、メソッド参照によるラムダ式の置き換えが出来なくなってしまいます。
Mono<ServerResponse> g(ServerRequest req) { return f().map(this::h); } ServerResponse h(Optional<String> strOpt) { if (strOpt.isEmpty()) { logger.info("str is not found."); return ServerResponse.notFound(); } var str = strOpt.get(); logger.info("str is {}.", str); return ServerResponse.ok(); }
これは先ほどのラムダ式をメソッドとして抽出しただけで、やっていることは同じです。
ラムダ式の中身が複雑になる場合は自然にこういう書き換えはやりたくなるとおもいますが、ラムダ式を例外とするだけでは足りないことが分かります。
まぁ、 public
以外にはルールは適用しない、みたいな例外を追加するようなことは考えられますが、いっそあきらめて Optional
を引数に使わない、なんてルールやめてしまった方がいいと思います。
いやいや、複雑になったとしても取得処理を呼び出し側でやって、メソッドの引数には Optional
を許さない!という態度もなくはないです。
Mono<ServerResponse> g(ServerRequest req) { return f().map(strOpt -> { if (strOpt.isEmpty()) { logger.info("str is not found."); return ServerResponse.notFound(); } var str = strOpt.get(); logger.info("str is {}.", str); return h(str); }); } ServerResponse h(String str) { // strを使った複雑な処理 return ServerResponse.ok(); }
こんな感じですね。
値の有り無しの振り分けをラムダ式側、処理自体をメソッド側と役割を分けられるため、なくはないです。
ただ、今回の例は Optional
が1つでしたが、これが2つ、3つと増えてくると、ラムダ式内が複雑になっていくため、そこにテストを書くためにメソッド化したくなります。
その際は、やはり Optional
を引数に持つメソッドが欲しくなります。
このように、 「Optional
を引数に使わない」というルールは実践的には無意味です。捨てましょう。
「フィールドに Optional
を使わない」というルールの方はもうちょっとだけ分からなくもない理由付けがあります*2が、
こちらもすべてのクラスがシリアル化対象なわけでもないので、個人的には無意味だと思っています。
Terraformの文法について?
最近Terraformを勉強してたんですが、日本語でTerraformの文法について説明しているものが見当たらなかったのではまった(?)ところまで書いてみました。
Terraformの文法?
正確にはHCL(HashiCorp Configuration Language)の文法です。 TerraformというツールにHCLという文法に従って書かれたファイルを食わせると、 その内容(意味)をツールが読み取って実行します。
設定ファイル
設定ファイルのトップレベルに書けるのは、コメントを除くと次の3つの要素です。
- 属性
- ブロック
- 1行ブロック
設定ファイルにはこれらを複数(0個でもいい)並べられます。 例えば、ブロック、属性、属性、ブロック、1行ブロック・・・のような書き方が許されているということです。
これを公式ドキュメントでは
ConfigFile = Body; Body = (Attribute | Block | OneLineBlock)*;
ブロック
ブロックの定義はこのようになっています。
Block = Identifier (StringLit|Identifier)* "{" Newline Body "}" Newline;
さっきより複雑ですが、要は
terraform { required_version = "0.12.26" }
とか
provider "aws" { region = "ap-northeast-1" }
とか
resource "aws_instance" "hoge" { ... }
とかしているのはすべてこの「ブロック」です。
terraform
構文や provider
構文や resource
構文などと個別に定義されてはいませんでした。
ということで、HCLは大体がブロックでできています。
ブロックの中身に Body
が出てきますが、 Body
は属性かブロックか1行ブロックのいずれかの複数の並びでした。
つまり、ブロックはネスト出来ます。
# 外側のブロック resource "null_resource" "hoge" { # 内側のブロック provisioner "local-exec" { # これは属性(後述) command = "..." } }
そして個人的に衝撃だったのは、例えば resource
ブロック、こうも書けます。
resource aws_instance hoge { ... }
今まで打ってきたダブルクォートを生まれる前に消し去りたい。
もう一度ブロックの定義を見てみましょう。
Block = Identifier (StringLit|Identifier)* "{" Newline Body "}" Newline;
(StringLit|Identifier)*
となっている部分に注目してほしいのですが、
これは「(文字列リテラルか識別子)が0個以上並ぶ」ことを意味しています。
provider "aws" { region = "ap-northeast-1" }
この "aws"
が文字列リテラルで、
provider aws { region = "ap-northeast-1" }
こっちの aws
が識別子です(provider
も識別子ですね)。
そしてこの部分を文字列リテラルで書いても識別子で書いても同じ意味です。
もっと早く公式の仕様を確認すべきでした。
他に面白い構文も特にないしやる気も尽きたので、ここまでにしておきます。 Terraform、ダブルクォートがだるいと思って避けている人もいるんじゃないかなと思うんですが、 別に書かなくていい場所になぜか書いている人が多いだけだよ、ということは広まってほしいですね。 公式のExample Usageがダブルクォート付きで書いているので、 お作法としては書くべきなのかもしれないですが面倒だし芋っぽいので自分は書かないことにします。
余談
Terraformは独自言語にもかかわらず、構文自体の説明は驚くほど少なくてびっくりします。 構文がシンプルなので説明不要でなんとなく使えるというのもあるんでしょうね。
CloudFormationはその点、文法はYAMLです、で説明がほぼ終わるのは利点ですよね。 関数とか独自機能もありますけど、YAML的にはvalidを保ったままの拡張なので、YAMLさえ知っていれば構文について覚えることは少なくて済みます*2。
WSLへのOchaCamlのインストール方法
基本的にはOchaCamlの公式ページに従えばいいんですが、環境をWSL(Ubuntu)に固定し、日本語でまとめておきます。
準備
makeとgccが入っていない場合はインストールしておきます。
sudo apt install make gcc
インストール方法
ホームディレクトリで作業します。
cd wget http://caml.inria.fr/pub/distrib/caml-light-0.75/cl75unix.tar.gz tar xpfvz cl75unix.tar.gz wget http://pllab.is.ocha.ac.jp/~asai/OchaCaml/download/OchaCaml.tar.gz tar xpfvz OchaCaml.tar.gz patch -p0 < OchaCaml/OchaCaml.diff cd cl75/src sed -i -e 's/ -no-cpp-precomp$//' Makefile make configure make world
起動方法
cd ./OchaCaml/ochacaml
これでREPLが立ち上がります。
第1回継続勉強会を開催しました
最近自分の中で継続熱が高まっているので、継続の話をできる場所が欲しくなりました。 とはいっても、そもそも継続の知識を持っている人が少ないので、そこを広めるところから始めることにしました。 社内でも継続を理解したいという人がおり、その人たちも勉強会を開きたいという話をしていたため、継続勉強会を開くことにしました。
とりあえずの目標としては、浅井先生のshift/resetプログラミング入門(PDF)を理解するところまでとしました。 第0回は社内で試しにどこまで行けそうかを見てみて、6回で1章と2章が終えられそうだったので、隔週開催でまずは6回やってみることにしました。
ここでは第1回でやった内容を紹介します。
やったこと
会の趣旨の説明と、テキストを2.5まで進めました。 思ったよりも進みが早く、用意していたメモが尽きかけました。 このあたりの話を理解するのに結構時間がかかったので、衝撃的でした。
shift/resetプログラミング入門メモ
以降はshift/resetプログラミングを読み進めるために補足説明等が必要な部分を補うためのメモです。 第1回継続勉強会では、これをもとにすすめました。 ので、単体で読んでも色々と繋がっていないです。 副読書としてどうぞ。
全体
読み進めるうちに「継続ってなんだっけ?」となったら、継続を「後続処理」と読み替えるといいかもしれません。
概要
継続の概念はどのプログラミング言語においても現れる普通の概念である
とありますが、「どんな言語でも、どこでも"考えられる"概念」であり、どんな言語でも現れるかというとそうではないように感じまし。 実際、継続をファーストクラスとして扱える言語は限られているため、後述の継続渡しスタイルを用いない限りはプログラム中で明には扱えません。 分岐や例外やgotoは、あくまで「継続という考え方でとらえることもできる」というものであり、それらを使うことがすなわち継続という概念を使っていることにはならないと思っておいた方が気が楽になると思います。
はじめに
普通のブロック構造では書きにくい制御構造
複雑な計算を書こうと思うと、しばしば普通のブロック構造では書きにくい制御構造を扱わなくてはならなくなる。
とありますが、具体的な例がありません。
書きにくいかどうかは別にして、例えば多重ループからの break
や continue
による脱出は、
「ラベル構文」と「脱出構文」を組み合わせて実現することが多いです。
// C# outer: // ラベル構文 while (true) { while (true) { if (cond) break outer; // 脱出構文 ... } ... }
継続であれば、「継続」という仕組み1つで成立するため、よりシンプルになります。
// C#に継続を入れたつもりの言語 callcc(k => { while (true) { while (true) { if (cond) k(); ... } ... } k(); });
callcc
は、呼び出された時点「以降の処理」を関数に変換(これを継続と呼ぶ)し、callcc
に渡された関数に継続を渡して実行します。
言葉だと分かりにくいのでコードで説明します。
var res = 1 + callcc(k => k(2)) + 3;
このコードで k
に渡されるのは、 callcc
を呼び出した時点以降の処理が関数に変換されたものなので、
「callcc
の結果に3を加える」となります(1を足す処理は callcc
を呼び出した時点では終わっているため、継続には含まれません)。
処理は以下のように進みます。
callcc
呼び出し以前の処理が実行されるcallcc
呼び出し以降の処理が関数化されるcallcc
に渡した関数が呼び出され、引数としてstep2で関数化されたものが渡されるcallcc
内に渡した関数の中で引数(継続)に値を渡すと、callcc
呼び出し自体の値としてそれが使われて以降の処理が実行される- つまり、継続を起動すると
callcc
を呼び出した場所に「脱出」する
- つまり、継続を起動すると
上記のコードでは継続 k
に 2
を渡しているので、callcc
呼び出し自体は 2
となります(step4)。
結果、上記コードは var res = 1 + 2 + 3;
と同じ意味のコードになります。
これを踏まえて再度多重ループからの脱出のコードを見てみましょう。
callcc(k => { while (true) { while (true) { if (cond) k(); ... } ... } k(); });
このコードでは、何らかの条件 cond
が true
に評価された場合に継続を起動しています。
継続を起動すると callcc
呼び出しから脱出するため、2重の while
を break
するのと同じ意味になります。
継続を使うとシンプルになる他の例として、高階関数とラムダ式を用いて制御構文を模倣、拡張するテクニックが広く知られていますが、
C#をはじめとした多くの手続き型言語では、この方法によって作られた関数は制御構文とは一部異なる振る舞いになります。
例えば、 foreach
によるループを模倣する場合、C#では
public static void ForEach<T>(this IEnumerable<T> xs, Action<T> a) { ... }
のような関数を定義することで、
foreach (var s in xs) { Console.WriteLine(s); }
の代わりに
xs.ForEach(s => { Console.WriteLine(s); });
のように書けます*1。
しかし、このラムダ式の中で return
を使った場合の挙動は、組み込みの foreach
を使った場合と ForEach
を使った場合では異なるものとなります。
public void F1(IEnumerable<string> xs) { foreach (var s in xs) { if (s == "exit") return; // F1からreturn Console.WriteLine(s); } } public void F2(IEnumerable<string> xs) { xs.ForEach(s => { if (s == "exit") return; // (F2ではなく)ラムダ式からreturn // foreach内でのcontinueと同じ意味 Console.WriteLine(s); }); }
これも、 return
ではなく継続を言語機能として提供することでシンプルに解決できます。
// C#ではない何か public void F3(IEnumerable<string> xs) { callcc(k => { xs.ForEach(s => { if (s == "exit") k(); // F3から脱出(F1の例と同じ挙動) Consolw.WriteLine(s); }); k(); }); }
さらに、 continue
も継続で実現できます。
// C#ではない何か public void F4(IEnumerable<string> xs) { xs.ForEach(s => { callcc(k => { if (s == "exit") k(); // 今回のループからの脱出 Console.WriteLine(s); k(); }); }); }
この例では break
と return
が同じ意味になるので説明しませんが、それらが異なる意味になる場合でも継続で実現可能です。
継続渡し形式
Continuation Passing Styleの和訳で、継続渡しスタイルとも言われます。CPSと略されます。 継続渡し形式でないものを、直接形式(Direct Style)と呼びます。
Direct Styleの例
double F(double x, double y) { double a = Math.Pow(x, 2); double b = Math.Pow(y, 2); return Math.Sqrt(a + b); }
CPSの例
戻り値を使わず、すべてコールバックを使い、コールバックの引数として結果を渡すようにします。
void F(double x, double y, Action<double> k) { PowCps(x, 2, a => PowCps(y, 2, b => SqrtCps(a + b, result => k(result)))); }
SchemeやStandard MLのcall/cc
継続を扱う命令で最も有名なのは Scheme や Standard ML の call/cc である。
とありますが、Standard MLの仕様にはcall/ccは含まれていないはずで、 あくまで処理系(SML/NJやMLton)が実装している機能です。
call/ccの使いにくさ
call/cc
に渡す関数の引数(今までの例での k
)が継続ですが、 call/cc
では受け取った継続はプログラム終了までの処理を含んでいるため、この継続を起動すると呼び出し元には戻ってきません。
これは、継続の範囲が「以降のプログラム全体」となっているためです。
例えば、以下のプログラムでは、k()
以降に書いた処理は実行されません。
// C#ではない何か public void F() { callcc(k => { k(); Console.WriteLine("このコードは実行されない。"); }); }
多相の型システム
JavaやC#でいう、ジェネリクスを持っているということです。
変更可能なセル
再代入可能な変数と思えば大丈夫です。
shift/resetプログラミング
3 + [・] - 1
元のプログラムはこうでした。
3 + 5 * 2 - 1
このプログラムで次に実行すべき部分は 5 * 2
の部分です(加減算よりも乗算の方が優先度が高い)。
この「次に実行すべき部分」をhole(穴という意味)にしたプログラムはこうなります。
3 + [・] - 1
これを callcc
を使ってよりプログラムらしく表現するとこうなります。
3 + callcc(k => k(5 * 2)) - 1
継続は、「holeの値を受け取ったら、その後の計算を行う」という意味で関数と似たような概念である。
とあるように、継続 k
はここでは関数として表現されています。
継続処理を実行することをこのテキストでは「継続を起動する」と表現します。
上の例では、10(5 * 2の結果)を渡して継続を起動しています。
このプログラムにおいて k
は、以下のような定義になっていると考えられます。
Func<int, int> k = (int result) => 3 + result - 1;
callcc
は、その時点の継続を取り出し、渡されたラムダ式の引数に関数として渡してくれます。
取り出す以外にも、切り取るや取り除く、取るなどと表現することもあります。
raise Abort
C#でいう throw new Exception();
です。
練習問題1
- OchaCamlでは二項演算子は右の項から実行する点に注意してください。
if cond then a else b
は、C#での条件演算子cond ? a : b
に相当します。また、=
は==
に、^
は+
(文字列の連結)に相当します。fst
は2要素タプル(a, b)
の1要素目を返します。また、let x = y in z
はvar x = y; z
に相当しますが、C#とは異なり式であり、z
の評価結果がlet
式全体の評価結果となります。string_length
は文字列の長さを返し、string_of_int
は数値を文字列に変換します。
reset (fun () -> M)
C#風に書くと reset(() => M)
です。
M
の中に、後述する shift
を含まない場合、M
と書いた場合と同じです。
var s1 = reset(() => "hello"); // 下と同じ var s2 = "hello";
Mを実行中の継続
callcc
的なもので継続を取り出したように、reset
で区切られた継続を取り出せる関数が shift
です。
「Mを実行中の継続」というよりは、「shift
によって取り出される継続」といった方がより正確かもしれません。
try ... with Abort -> ...
C#での try { ... } catch (Exception e) { ... }
ですが、式なので値を持つという点が異なります。
無理やり表現するなら、次のような感じでしょうか。
Func<T>(() => { try { ... } catch (Exception e) { ... } })();
多値について本気で考えてみた
先日のエントリの反応として、多値の批判をしているように受け取られた方がいました。 実際には、多値の批判をしているのではなく、Go言語の「多値とそう見えるけど違うものがある」という仕様を批判したものでした。
また、タプルにこだわっているという受け取り方をした方もいました。 このエントリでは、「タプルにこだわっているのではない、多値にこだわっているのだ」ということを説明しようと思います。 このエントリで出てくるコードは言及がない限り妄想上のもので、実際の言語のコードではありません。
長いから3行で。
- スタックマシンと多値は仲良し。継続と多値も仲良し。
- 多値は多値、タプルはタプル、みんなちがってみんないい。
- 多値とは、カンマで区切られた単なる複数の値だよ。妄想だけどね。
これで満足して仕事に戻っていただいて構いません。以下オマケ。
多値とタプルの違い
まず、多値とタプルの意味的な違いについてをはっきりさせておきましょう。 ただし、多値はタプルと違って扱える言語が少ない*1うえ、各言語での違いもそれなりに大きいため、ここで紹介する違いは参考程度に考えてください。
他の値の一部になれるかどうか
タプルは何の制約もない、単なる値です。 そのため、他の値の一部になれます*2。 当然、タプルの要素にタプルを入れるという風に、入れ子構造も取れます。
それに対して、多値は他の値の一部にはなれません。 例えば、クラスのフィールドに多値を含むこともできませんし、多値の要素として多値を含むこともできません。 これを、制約の付いた型と見なすこともできますが、単に多値はファーストクラスのオブジェクトではないと考えてもよいでしょう。
多値は制限されたタプルなのか
ここまででは、多値は制限されたタプルであり、多値には何のメリットもないとしか思えないかもしれません。 しかし、多値には効率という大きなメリットがあるのです。 その話に入る前に、多値と相性のよいものについて見ていきましょう。 スタックマシンと、継続です。
スタックマシンと多値
まずはスタックマシンです。 スタックマシンというのは、スタックを用いて計算を行う計算機のことを言いますが、ここでは詳細には踏み込みません。 Java仮想マシンや、.NETのCLRや、RubyVM(旧称YARV)などもスタックマシンをベースにしています。少なくとも30億のデバイスでスタックマシンは動いていることになりますね。すごい。
スタックマシンでの関数呼び出し
スタックマシンでは、引数をスタックに積んでから関数に処理を移すだけで関数呼び出しができます。 例えば、Javaで次のようなメソッドを書いたとしましょう。
public static int add(int a, int b) { return a + b; }
このメソッドを add(10, 20)
のように呼び出した場合、以下のようなバイトコードが出力されます。
bipush 10 // byte範囲に収まる数値10をpush bipush 20 // byte範囲に収まる数値20をpush invokestatic add // addメソッドを呼び出す
これをスタックの状態を含めて図にすると、このような感じになります。
| | bipush 10 | | bipush 20 | 20 | invokestatic add | | | |---------->| 10 |---------->| 10 |----------------->| 30 | +----+ +----+ +----+ +----+
まさに、スタックに引数を積んでから関数が呼び出されています。 そして、結果はスタックに積まれます。
関数から戻る場合はどうでしょうか。
上で作った add
のバイトコードを見てみましょう。
iload_0 // 0番目の引数を整数としてpush iload_1 // 1番目の引数を整数としてpush iadd // スタックに積まれた2つの整数を加算、結果をpush ireturn // スタックに積まれた数値を戻り値としてメソッドからreturn
add(10, 20)
で呼び出された場合のスタックの移り変わりはこのようになります。
| | iload_0 | | iload_1 | 20 | iadd | | ireturn | |-------->| 10 |-------->| 10 |----->| 30 |--------> +----+ +----+ +----+ +----+
スタックマシン上での多値の表現
スタックマシンでは、多値はスタック上に積まれた(複数の)値でしかありません。 n個の値を積んで関数を呼び出すということは、n値を入力にする関数を呼び出すということですし、 m個の値を積んだ状態で関数からreturnするということは、m値を出力として関数を終えた、ということです*3。
これはとてもきれいですよね。 例えばGo言語がスタックマシン上で実装されているとしたら、
func add(a int, b int) int { return a + b } func f(a int) (int, int) { return a, a * 2 } add(f(3))
は、
push 3 call f // 3が積まれた状態でfを呼び出す。実行が終わるとスタックに値が2つ積まれている。 call add // 3と6がスタックに積まれた状態でaddを呼び出す。
と表現されていることでしょう。
継続と多値
継続について説明しだすと延々と横道にそれていってしまうので、 解説ページ へのリンクだけ置いておきます。 未完成部分が埋められることはもはやないと思われます。残念。
さて、継続と多値の関係ですが、継続とはつまるところ「関数からのreturnを、returnした後の処理を表す関数呼び出し」と考えてしまおう、ということです*4。 このとき、「継続に渡される引数が複数個ある」ということの意味を考えてみましょう。 「継続に渡される引数」は、「returnされた値」に対応しますので、これが複数個あるということは「複数の結果が関数から返ってきた」ことを意味します。 つまりは多値です。
すべてを継続で考えれば、returnはすべて関数の引数になります。 その世界においては、多値とは単に継続(もしくは関数)に渡す引数が複数あるというだけとなります。 これもとてもきれいですね。
ちょっと無理やりですが、Go言語であればこのようなイメージの世界です。
// スタックマシン上での多値の表現で使ったプログラムをreturnやGo言語の多値なしに表現してみた例 func add(a int, b int, k func(int)) { k(a + b) // returnの代わりに継続を呼び出す } func f(a int, k func(int, int)) { k(a, a * 2) // returnの代わりに継続を呼び出す(多値!) } func main() { f(3, func(a int, b int) { // fは多値を関数の引数として渡してくる add(a, b, func(x int) { fmt.Println(x) }) }) }
returnもGo言語の多値も使っていませんが、やっていることはGo言語の多値を使ったコードと同じです。
ちなみに、継続を扱える言語であるSchemeでは、多値を作る関数をこう定義できます。
(define (values . xs) (call/cc (lambda (k) (apply k xs))))
call/cc
で values
呼び出し以降の処理を切り取って k
とし、その継続に values
の引数を入れるという、まさに「継続の引数が多値である」をそのまま表したコードになっています。
きれいだ・・・!
関数
さて、ここまでは多値と相性のよいものを見てきました。 ここからは、関数について少し考えてみます。
関数型プログラミング言語と関数
メジャーな関数型プログラミング言語では、関数は1入力1出力というモデルです。
多入力したい場合は「関数を返す関数」のように関数を値として扱えるようにしたことで解決しています(カリー化というやつ)。
// F# // let add x y = x + yと同じ let add = fun x -> fun y -> x + y
多出力したい場合はどうでしょうか。 これも、「関数を受け取る関数」により実現できます。 これはつまり、継続で見た方法です*5。
// F# let f = fun x -> fun k -> k x (x * 2) // (k x)で返ってきた関数に(x * 2)を適用 // シンタックスシュガーを使うと、 // f 3 (fun x y -> add x y) // とか // f 3 add // とか書ける f 3 (fun x -> fun y -> add x y // (add x)で返ってきた関数にyを適用 )
このように、関数型プログラミング言語では1入力1出力の関数だけですべてを表せる世界を作っているわけです*6。 これはこれできれいですね。
手続き型プログラミング言語と関数
Go言語を除いた多くの手続き型プログラミング言語では、関数は多入力1出力です。 なぜ入力は複数許すのに、出力は1つか許してないのでしょうか。 自分はC言語が1つの戻り値しか許さないようになっていたのをずっと引きずってきたのではないか、と考えています。
アセンブリレベルまで降りていけば、そもそもレジスタを使って複数の値を返すようなサブルーチンなんかは普通に書けるわけです。 x86であれば、divはeaxに商を、edxに余りを格納しますが、これも多値を返していると見なせます。
アセンブリレベルまで降りれば多値が使えるのに、今までのプログラミング言語ではそれを有効活用してこなかったことになります。 これは、手続き型プログラミング言語が計算機を効率よく使えるように進化してきたことを考えると、少し不幸な感じがします。 Go言語は、そういった世界をぶち壊すインパクトを持った言語だと思います*7。
タプルと多値(と手続き型プログラミング言語)
多値はネストができません。他の値の要素となることもできません。 この制約によって多値は何を手に入れたのでしょうか。
それは、効率です。 多値と同じようなものとみられることもあるタプルですが、タプルはあくまで1つの値に複数の値をパックしたものです。 パックする処理(タプルの構築)も、アンパックする処理(タプルの分解)も、どれもタダというわけではありません。 言語処理系において、タプルの効率を上げようとする試みはいくつもありますが、タプルが値である以上、すべてのタプルを最適化できるわけではありません。
それに対し、多値は単なる複数の値であり、それ自体は値ではありません(スタックに積んであるだけ、レジスタに並んでいるだけ)。 そのため、パックやアンパックなどとは無縁の世界で生きていられます。
手続き型プログラミング言語でも関数がファーストクラスの値として使えるような言語が増えてきましたが、 手続き型プログラミング言語は本来、計算機を効率を程よく保ったまま抽象的に扱えるようにした言語であるべきではないでしょうか(ただし異論は認める)。 その場合、関数だのタプルだのをファーストクラスで扱えることにこだわらず、効率よく扱えるものを扱うにとどめるという割り切った言語があってもいいと思います。
ただ、ユーザー定義できる型とジェネリクスがあるとそれだけでタプルが作れてしまうので、多値がないがしろにされがち、というのはあるかもしれません。
多値とは
さて、多値とは何者でしょうか。
- 「単なるスタックに積まれた値だよ」
- 「単なる継続の引数だよ」
- 「Go言語の多値が多値だよ」
色々と意見はあるでしょうが、ここからは「カンマで区切られた単なる複数の値だよ」という妄想の世界の話です。 ちなみに、上の3つだと一番下が一番近いです(が、別物です)。
多値かくあるべし!
架空の言語(WillGoとでもしておきましょう)を考えます。 この言語では、多値はカンマで区切られた単なる複数の値です。 どういうことか見てみましょう。 まずは、多値を返す関数です。
// 型と値の間にはコロンを置く(趣味) // 多値を出力する関数は、出力する型をそれぞれカンマで区切って表現する func f(a: int): int, int { return a, a * 2 // 多値を返している。 } x, y := f(3) // x, yというのは多値を表している。
多値はカンマで区切られたものとして表現されていますね。
多値を受け取る関数も見てみましょう。
// a: int, b: int というのは、多値を受け取る関数であることを表している。 // 関数の出力で多値を表す場合と同じ表現であることが分かる。 func add(a: int, b: int): int { return a + b } result := add(1, 2) // 1, 2というのは多値を表している。
当然、多値を返す関数の結果をそのまま多値を渡す関数に渡せます。
result := add(f(3)) // 多値を渡す関数に多値を返す関数の結果をそのまま渡している。
ここまではいい感じですね。
多値かくあるべし・・・?
さて、多値は「カンマで区切られた単なる複数の値」でした。 ここから、妄想らしくなっていきます。
// 4引数版addを定義。 func add4(a: int, b: int, c: int, d: int): int { return a + b + c + d } result := add4(1, 2, 3, 4) // 1, 2, 3, 4は多値。
この add4
に対して、こんな呼び出しはどうでしょうか。
res1 := add4(1, 2, f(3)) res2 := add4(1, f(2), 3) res3 := add4(f(1), f(2))
くどいようですが、多値は「カンマで区切られた単なる複数の値」でした。 であるならば、「そこに展開した結果がvalidであればvalidとする」としてもいいと思いませんか。 きれいだ・・・!
きれいな多値の分かりにくさ
さて、この架空の言語ですが、関数呼び出しを見ても引数の個数が分からないという問題があります。
res3 := add4(f(1), f(2)) // 2引数関数に見える
今どきのIDEなら、コード上に書かれていない文字列を表示するくらいやってのけるので、IDE前提であれば使えるかもしれません。
// 上のコードは、IDEで開くとこう見える(実際に書いてない部分はグレーアウト)。 res3 := add4(a, b=f(x=1), c,d=f(x=2))
もしくは、関数名さえ気を付ければそれほど問題にはならないかもしれません。
ちなみに、実在するGoという言語はこの問題に足を片方入れています。
// Go言語 res := f(g()) // さて、fは何引数関数でしょう?
そこまでやったのであれば、きれいな多値が欲しくなりませんか?
可変長引数と多値、もしくは可変長戻り値と多値
ここまでくると、行くところまで行ってみたい気がしますね。 引数を何個でも指定できる関数というものがあります。
func sum(xs: ...int): int { res := 0 for _, x range xs { res += x } return res } result := sum(1, 2, 3)
では、戻り値を何個でも指定できる関数というのはどうでしょうか。
func f(b: bool): ...int { if b { return 1, 2 } else { return 4, 5, 6 } }
これは、通常の手段では受け取れない関数となります。 この結果を使うためには、可変長引数の関数が必要です。
result := sum(f(true))
または、コンパイル時定数が渡された場合のみ受け取れるようにするのも面白そうです*8。
a, b, c := sum(f(false)) // OK d, e, f := sum(f(true)) // コンパイルエラー
スライスに変換する組み込み関数があれば(効率以外は)問題ありません。
xs := valuesToSlice(f(true)) ys := valuesToSlice(f(false))
これで、多値を「カンマで区切られた単なる複数の値」としてみなしても成り立ちそうだということがなんとなくわかっていただけたかと思います(便利だとは言っていない)。
このように、多値は多値で面白い世界が広がっているのです。 Go言語の多値は始まりでしかありません。 みなさんも自分だけの多値の世界を考えて、どんどん多値のすばらしさを世界に発信していきましょう!
参考ページ
- Scheme:多値: Schemeでの多値についての解説ページ。
- なんでも継続: 継続についての解説ページ。未完。
- 思考実験: returnを関数と思ってみる話: JavaScript風言語でreturnを関数と思ったらどういう世界があるのか考察したページ。
- なぜ、多値関数は人気がないのだろう - 檜山正幸のキマイラ飼育記: 多値が流行っていない理由の考察ページ。
- 他にもあったけど忘れた・・・
*1:メジャーな言語だとScheme/Common LispとGoくらいではないでしょうか。何をメジャーに入れるかという問題はありますが。※Luaも多値を持っているようです。Twitterで教えてもらいました。※GHC拡張にもUnboxed Tuplesという構文拡張があるようです。これもTwitterで教えてもらいました。
*2:クラスのフィールドとして持たせられる。
*3:ただし、上で例に挙げたJava仮想マシン(や、他の仮想マシン)では、m個の値を積んだ状態で関数からreturnすることを許していません。
*4:http://www.kmonos.net/wlog/95.html#_1109090307 という記事を思い出したので置いておきます。この記事では、returnを関数と見なすとどうなるだろう、という思考実験をしています。
*5:多入力と多出力で戻り値の位置に関数がくるか、引数の位置に関数がくるかが入れ替わるのも面白いですね。双対性というやつでしょうか。
*6:念のため: 実際にこんなコードは書きません。
*7:しかし、前のエントリでも書いたように、Go言語は多値っぽいけど多値じゃないものを入れてしまったのでそこはすごく残念。
Go言語のイケてない部分
最近色々あって仕事でGo言語を使っています。 色々割り切っている言語なので、こんなこと言ってもしゃーないんですが、言語設計はミスってるんじゃなかなぁ、と思わざるを得ない点が多々あります。 使い始めて1か月くらいなので間違ったことを書いているかもしれませんので、何かあれば指摘していただけるとありがたいです。
本文ではネガばかり羅列していますが、ランタイムとツール周りは気に入っています。 Goのランタイムを使う、もっと洗練されたAlt Go的なものがあるといいのに(もしくはジェネリクスのったGo2を早くリリースしてほしい)、と思う日々です。
追記: なんか意図とは違った受け取られ方をしている方もいるので追記します。 この記事はあくまで、「Go言語を学ぶにあたって躓いた点」を列挙し、まとめ、理由を考えてみる(教えてもらう)ために書いたものです。 Go言語自体はDisってますが、Go言語ユーザーをDisる目的では書いていません。
また、以下のような意見も見られました。
まずひとつめについてですが、自分はPythonやRubyのようないわゆるLL使いではありません。 典型的でありがちな批判かどうかは自分では判断できませんが、観測範囲でこのような批判がまとまっているものを見た覚えはないです。 そのため、この記事を書くためにいろいろと調べましたし。 それと、多値にこだわっているのではなく、言語仕様が初学者が躓きやすい(し、理由を調べにくい)ようになっているのでは?という指摘こそが言いたいことです。
ふたつめの慣れれば問題ないという指摘も、慣れるまでの障壁の話を書いているのでこの記事に対しては意味を持ちません。 人にもよりますが、ルールの推測しずらい(シンプルでない)ものを覚えるのは苦手なので、自分と同じような人間が躓かないように気にしていただければと思います。
みっつめですが、避けられない理由が「いろいろ」あるのです。察してください。
よっつめのIssueにすればよい、という意見ですが、このエントリはあくまで「ここが分かりにくい」というところを(理由があればそれも込みで)まとめたものであって、 言語仕様を自分の思い通りに変えたいという話ではありません。 Issueにすればどうなるというのでしょうか。 互換性を壊すということで却下されてそれで終わりだという確信があります。 それよりは、エントリにまとめて周知した方が生産的だと思いますし、IssueでやりあうほどのGo愛を自分は持ち合わせておりません。
多値まわり
Go言語には多値という機能*1が言語に組み込まれています。 しかし、これが中途半端なうえ、多値っぽい構文で多値ではないものがいくつかあるため、初心者を混乱させる原因になっています。
まず、多値はこのように使います。
// 多値を返す関数minmax func minmax(x int, y int) (int, int) { if x < y { return x, y // 条件演算子がないのも割り切りだとわかっていてもつらい。 } return y, x } // 多値の受け取り min, max := minmax(20, 10) fmt.Println("min:", min, ", max:", max) // => min: 10, max: 20
これを念頭に読んでください。
追記: 多値に関して、別記事としてまとめました。
こちらを読んでいただければ、多値はダメだからタプルを入れるべきだった、という話をしていないことが分かっていただけるかと思います。
多値をひと塊で扱える例外: 多値を多値として返す
Go言語では多値は基本的にひと塊のオブジェクトとして扱えません(ので、他の言語のにあるようなタプルとは違います)。
tuple := minmax(20, 10) // コンパイルエラー
しかし、例外がいくつかあります。 ひとつめが、他の関数の戻り値をそのまま自分の関数の戻り値として返す場合です。
func minmax2(x int, y int) (int, int) { return minmax(x, y) // そのまま返す }
この場合、結果が同数*2かつすべて同じ型である必要があります。
多値をひと塊で扱える例外: 同じ形の多値を取る関数にそのまま渡す
ふたつめが、同じ形の多値をとる関数にそのまま渡す場合です。
fmt.Println(minmax(20, 10)) // => 10 20
fmt.Println
が interface{}
という何でも渡せる型を可変長引数として複数受け取れる関数なので、 minmax
関数の結果をそのまま渡せています。
しかし、可変長だからと言ってこのようなことはできません。
fmt.Println("min, max:", minmax(20, 10)) // コンパイルエラー
できてもよさそうなものですが、なぜかできません。 出来ないようにしている理由はわかりません。
多値っぽい構文なのに多値ではない機能: for range構文
主にコレクションを走査するときに使う for range構文というものがあります。 例えば、Go言語にはジェネリクスがないのでいわゆるmap処理を行いたい場合、
ys := make([]int, len(xs)) for i, x := range xs { ys[i] = x * 2 }
のように書きます。
i, x
の部分は多値のように見えますが、これは多値ではなく構文です。
多値では値を受け取らないということはできませんが、この構文は x
の側(この記事では2nd valueと呼びます)を無視できます。
min := minmax(20, 10) // コンパイルエラー
ys := make([]int, len(xs)) for i := range xs { // OK. xsの要素ではなく、インデックスが取れる ys[i] = i }
要素の方ではなくインデックスの方が取れるのが使いにくい感じがしますが、これはmapとの統一をしたかったためと思われます。 mapでは1st valueがキー、2nd valueが値となるので、mapでもスライスでも1st value側を取れるようにした、ということでしょう。 mapは値を走査するよりもキーを走査する方が多いでしょうから。
多値ではないものはまだあります。
多値っぽい構文なのに多値ではない機能: mapへのアクセス
Go言語では、mapへのアクセスにはスライスや文字列などと同様、 []
を使います。
その結果として、値のほかにキーが存在したかどうかを表す bool
値が受け取れます。
m := map[string]int { "a": 10, "b": 20 } n, ok := m["c"] // 2nd valueとしてbool値が受け取れる if !ok { n = 0 } fmt.Println("n:", n) // => n: 0
これも n, ok
の部分は多値に見えますが、多値ではありません。
まず、 ok
の部分が省略できますし、多値ではできる「他の関数にそのまま渡す」ができません。
func f(n int, ok bool) { } f(m["c"]) // コンパイルエラー
これは ok
が省略できることと両立できないからでしょう。
上のようなコードが許された場合、下のようなコードでどういう挙動にすればいいのかという微妙な問題が出てきます。
// 1st valueだけを表示すべきなのか、2nd valueも表示すべきなのか fmt.Println(m["c"])
多値っぽく見せるなら、省略なぞ許さずに多値にしてしまったほうがよかったと思います。
あり得ない話だとは思いますが、もしGoが演算子オーバーロードを許すようになったとしても、 []
の結果を多値にしては組み込みのmapと同じになりません。
ではどうするのか。省略可能を示す ?
みたいな機能を増やすんでしょうかね。
多値っぽい構文なのに多値ではない機能: 型アサーション
型アサーション(キャストのようなもの)も、mapへのアクセス同様に2nd valueとして成否を表す bool
値を返します。
func f(x interface{}) int { n, ok := x.(int) // 多値ではない if !ok { n = -1 } return n }
mapと同じことが言えますね。
多値っぽい構文なのに多値ではない機能: チャネルからの受信
同上なので略。
多値が使えると便利そうなのに使えないし、別の意味になる: switch構文
Go言語でのswitch構文には多値が使えません。
// コンパイルエラー switch minmax(20, 10) { case 10, 20: }
ですが、 case 10, 20
の部分は多値ではない意味として使われています。
switch 20 { case 10, 20: fmt.Println("10 or 20") default: fmt.Println("other") }
Go言語では、 case
はフォールスルーしませんので、複数の選択肢で同じ処理をさせたい場合は上のようにカンマを使います。
switch
で多値が使えたら、 ok
とそうでない場合とか、 err
が nil
かそうでないかなどで分岐処理が書けたのに、残念な文法の選択をしたと思います。
多値が使えると便利そうなのに使えない: チャネル
Go言語における同期処理の入力、出力と言えば、関数の引数とその結果*3です。 関数の引数は多値を許しますし、関数の結果も多値を許します。対称性が取れていますね。
Go言語における非同期処理の入力、出力と言えば、チャネルへの送信と受信ではないでしょうか。 しかし、チャネルへの送信にも受信にも、多値は使えません。対称性は取れていますね。でも、同期処理と非同期処理で統一性はないように思います。
なぜチャネルで多値が使えないかの理由は思い浮かびません。 実装が難しいとかなんでしょうか。CSPよりもアクター派なんで詳しくはわかりません。
コレクション
ジェネリクスがないので、基本的には言語組み込みのものしか使いません。 また、ジェネリクスがないので、いわゆるmap関数だとかは用意できないので基本的にはfor文を書くことになります。 他の言語では1行で書けることが、Go言語では3行~6行程度必要になることは多いです。 コンパイル速度のためとはいえ、今どきの言語としてはイケていない部分でしょう。
mapには2nd valueがあるのに配列, スライス, 文字列には2nd valueがない
多値の項でも言及した通り、mapへのインデックスアクセスは2nd valueを持ちます。 しかし、同じく言語組み込みのコレクションである配列やスライス、文字列には2nd valueがありません。
そのため、範囲外のインデックスアクセスが発生するとこれらはpanicを起こします。 効率のためかもしれませんが、そうであるならmapに対しても2nd valueを(多値と同じような構文で)用意するべきではなかったと思います。 それよりは、mapに対して組み込み関数で多値を返すようなものを用意してほしかったです(それか、配列やスライスなどにも2nd valueを用意する)。
:=
:=
は var
+ =
の略記法、だけではないんです。
シャドーイングと再代入
=
は再代入*4、 :=
はシャドーイング、という説明を見たような気がします。
が、これだけでは :=
の性質をすべて語っていません。
まず、Go言語では同一スコープでのシャドーイングはできません*5。
x := 10 x := 20 // コンパイルエラー
ですが、みなさんこんなコード書いてませんか?
x, err := f() if err != nil { return nil, err } y, err := g(x) // ...
これ、同一スコープに err
という同じ名前の変数があるので、シャドーイングできているようにも見えます。
しかし、実はこれは再代入なのです。
func f() (int, error) { return 0, nil } func g(x int) (string, string) { return "", "" }
としたうえでコンパイルすると、 string
は error
を実装していない、というコンパイルエラーになります。
また、 :=
では新たな識別子を導入しないとコンパイルエラーになるため、
x, err := f() x, err := g()
のようなことはできません。 これだと2行目では新たな識別子が導入されておらず、「short variable declaration(短い変数宣言)」にならないからです。
でも、短い変数宣言という名前なのに多値に対しては再代入が起こり得るというのはどうなんでしょうか。 そうするくらいなら、同一スコープでのシャドーイングを許してしまった方がよかったと思います。
このせいで error
しか返さないような関数が混ざると残念なことになります。
x, err := f() err := g() // コンパイルエラー err = g() // こうするか、 err2 := g() // こうする。上かな。
もし同一スコープでのシャドーイングを許していたらこう書けました。
x, err := f() err := g()
err
は :=
で受ける。統一されていると思いませんか。
追記:
ちなみにerrに何度も代入するところは「if err := g(); err != nil {}」と書くのでなにも問題ない。
— のぼのぼ☂️ (@nobonobo) 2018年11月8日
error
のみを返す関数は、if文のSimpleStmtで受け取る書き方を常時行えば問題ないとのこと。
処理がif文の中に紛れ込むのを完全には賛成できませんが、これは好みの問題でしょう。
同一スコープでのシャドーイングが欲しかったという意見は変わりませんが、今後はこの書き方で回避したいと思います。
別の意味に同じ記号
Go言語において、 :=
は実は2つの意味を持ちます。
- short variable declaration
- for range構文でのRangeClause
これらは、意味としてはとても似通っているのですが、別物です。
前者は3値や4値も当然受け取れますが、後者はインデックスと値の2値のみです*6。 前者は右項の値すべてを受ける必要がありますが、後者は2nd valueが必要ない場合は省略できます。
個人的には、for range構文に :=
は不要だったのではないかな、と思っています。
// コンパイルエラーだけどこれでよかったのでは? for i, x range xs { }
こうなっていない理由として考えられるのは、実はfor range構文では :=
のほかに =
も使えるからというのがあるのでしょう。
=
の方を使うと、ループの最後の値がループの外から参照できる、という利点があります。
func f(xs []string) { var i int var x string for i, x = range xs { // ちなみに、ここにはvarはそもそも置けない(ので、:=はvar+=の略記法だけではない) fmt.Println(i, x) } fmt.Println("last index:", i, "last value:", x) }
しかし、これは少々技巧的で、条件演算子すら省くGo言語の思想とは相容れないように思えます。
なので、for range構文ではシンプルに :=
の機能だけに絞って :=
自体は省略してしまった方がいろいろと分かりやすくなったのでは、と思います。
ちなみに、紛らわしいことに、C風のfor構文で使える :=
はshort variable declarationです。
// short variable declaration for i := 0; i < n; i++ { }
defer
スコープではなく、関数に属する
そのままです。罠ですよね。 一応、無名関数をその場で呼び出すことで回避できますが、スコープに属するようにしてくれればよかったのに、と思います。 そうなっていない理由は効率なのでしょうか。わかりません。
追記:
if xxx {
— いわた (@wonderful_panda) 2018年11月8日
defer con.Close()
}
とか書けた方がうれしいことあるやろ?だからdeferは関数に属するようにしたんやで
みたいなのをどこかで見た
Twitterで納得度の高い理由を教えてもらいました。
なるほど、これは確かにブロックスコープだと困りますね。
ループの中で defer
して死というのはまだ分かりやすいですし、この点に関してGo言語の選択は正解かもしれません。
パッケージ
階層構造を取れない
これもそのままです。 複数の粒度でのグルーピングは不要ということでしょうか。 地味に困ります。
struct / interface
構文上の違いがないのに区別が必要
型の表現方法(struct/interface)によってポインタを渡すべきかどうか変える必要が出てくる(効率を考えた場合の話)のに、それが構文上区別できないのはつらいです。
C#のように命名規約で回避するくらはしてくれてもよかったように思います。
それか、C言語のように struct
を明示するだとかでもこの際よかったかな、と思います。
echo
JSONメソッド
Go言語ではなく、echoというフレームワークに対する愚痴です。
echo(この名前もググラビリティ低い)ではレスポンスを組み立てるために Context
オブジェクトのメソッドを呼び出します。
そして、アクセスされた際に呼び出されるハンドラーは error
を返します。
だいたいこんな感じになるわけです。
e.GET("/path", func (c Context) error { return c.JSON(http.StatusOK, f()) })
これ見てどう思いますか。
c.JSON
を呼び出すことでレスポンスオブジェクトが作られ、それが error
インターフェイスを実装している、そう感じませんか。
どうやらechoの作者はそうではなかったようで、このメソッドで レスポンスに対してJSONの書き込みが走 るように作られています。
そして、書き込みに失敗したら error
を返すのです。
func f(c Context) ([]SomeResult, error) { // なんやかんや処理 if reason != nil { return c.JSON(http.StatusInternalServerError, reason) } return result }
とかやって、
e.GET("/path", func (c Context) error { res, err := f(c) if err != nil { return err } return c.JSON(http.StatusOK, res) })
とやったらどうなりますか。
そうです。 reason
が nil
ではなかった場合、reason
がレスポンスに書き込まれますが、レスポンスへの書き込み自体が失敗しなければ c.JSON
は nil
が返ってくるので、
ハンドラーの中の if
には入らず、再度 c.JSON
が呼び出され、ステータスOKでエラーレスポンス(reasonの内容)が返されます。
さらに、エラーレスポンスの後ろに []
というゴミが付いて しまいます。
なぜなら c.JSON
はレスポンスに書き込むだけだから。
ハンドラーの最後の return
で res
を書き込んでいますから。
多くの場合エラーがあれば res
は空ですから。
当然、レスポンスには []
が付いてきますよね。
・・・。
ドキュメントを読まなかった自分も悪いんですが、こういう関数に JSON
なんて宣言的に見える名前を付けてほしくなかった。
WriteJson
とか、操作的に見える名前ならこんな間違いはしなかった。
vim
テキストオブジェクトとの相性の悪さ
GoLandを使っているのですが、当然(?)Vim Pluginを使っています。 ですが、Goの構文は括弧を省略しているため、「if文の条件部分に対して操作」・・・あっ・・・できない! となることが多いです。 Vimmerに優しくない*7。
まとめ
細かいところではまだまだあるのですが、好みの部分も多い*8のでこのくらいにしておきます。 後半力尽きて適当アンド愚痴ですが、こんな感じで今のところGo言語イケてない。ダサい。という認識です。 この言語のどこがシンプルなんじゃ!むずかしいやろ!どちらかというとイージー方向の機能満載やろ!!!という気分です。 「いいやそこは見方が悪いんだ、こういう見方をするとイケてるんだぜ!」という意見があれば是非聞きたいです。 よろしくお願いします。
*1:他の言語でのタプルのようなもの。ただし、タプルのようにひと塊として扱えるものではなく、単に関数から戻るときに複数の値がスタックに残っているような状態と思った方がいいです。
*2:多値は2値だけでなく、3値でも4値でも返せます。
*3:ちなみに、戻り値型と呼ばないのは多値は(Goでは)型ではないからです。
*4:varでの=は除く
*5:例えばF#は同一スコープでのシャドーイングができているように見えるのですが、実はletがスコープを作っているので同一スコープでのシャドーイングを実際にしているわけではありません。見た目的には同一スコープのシャドーイングに見えるので置いておきましょう。
*6:チャネルは2nd valueを取りませんが、ここでは省きます。
*7:そもそもIntellij/GoLandのvim pluginって文字列リテラルの中でテキストオブジェクト使えないという致命的な欠点があってつらいんですが、それはまた別の話。
*8:型指定の書き方とか。