kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

Hello, Dart (2) - パーザコンビネータ

いつだったか、新しい言語を学ぶときのHello, Worldはパーザコンビネータを書くことにしてる、といった事を言った覚えがあります。実際、これまで学んできたたいていの言語でミニパーザコンビネータライブラリを書いてきました。というわけで、Dartでもパーザコンビネータライブラリを書いてみました。

もう何度も何度も書いてきたコードのため、考えるのは、その言語での標準的なデータ構造と関数に落とし込む方法だけです。というわけで、次のような感じになりました。クラスの定義方法とかコンストラクタの扱い、無名関数の文法、文字列の取り扱い、など基本的なことを一度に学べるので、パーザコンビネータという題材はやはり良いなと思います(自画自賛)。

感想:

  • 色々な面でかなりJavaプログラマフレンドリ。
    • 適当にJavaっぽい文法で書くとそれなりにパーズしてくれる可能性が高い。
  • 関数/メソッドの本体が式であるときに使える、 => 記法は便利。というか、これがないと不便。
  • 多相オペレータを定義できないのはちょっと不便。
    • Parser<Pair<T, U>> operator &<U>(Parser<U> that) みたいなのを書きたいが、これはDartでは許されないようだ。
  • Javaと違って、プリミティブ型を意識せずに済むのは楽。
  • コンストラクタの略記法が中途半端。フィールドへの代入コードを手動で書くよりは楽だが…
  • 関数の型の文法が、Cっぽい微妙さ。 Ret funArg(Arg1 arg1, ...) のように書くのだが、これだと関数を返す関数の型が綺麗に書けない…
    • typedef すればいいのだけど。
  • Javaでメソッドの型パラメタの宣言の位置がヘンテコだった問題は解決されている。

全体的に、Dartは、今の時代の言語としては平凡な感じがぬぐえませんが、その分、変な癖がないとも言えそうです。また、JavaプログラマDartを学ぶのはかなり簡単であるという 感想を持ちました。

abstract class ParseResult<T> {
  String next;
  ParseResult(this.next) {}

  T get value;
  bool get successful;
}

class Pair<T1, T2> {
  T1 item1;
  T2 item2;
  Pair(this.item1, this.item2);

  String toString() {
    return '(${item1}, ${item2})';
  }
}

class ParseSuccess<T> extends ParseResult<T> {
  T value;
  ParseSuccess(this.value, String next) : super(next);
  @override bool get successful => true;
  @override String toString() => 'ParseSuccess(${value}, ${next})';
}

class ParseFailure<T> extends ParseResult<T> {
  T value = null;
  ParseFailure(String next) : super(next);
  @override bool get successful => false;
  @override String toString() => 'ParseFailure(${next})';
}

typedef ParseResult<T> ParserFunction<T>(String input);

typedef U BiFunction<T1, T2, U>(T1 t1, T2 t2);

class Parser<T> {
  ParserFunction<T> fun;
  Parser(this.fun);
  ParseResult<T> call(String input) => fun(input);

  Parser<T> operator |(Parser<T> that) => parserOf(
    (input) {
      var r1 = this.fun(input);
      if(r1.successful) {
        return r1;
      } else {
        return that.fun(input);
      }
    }
  );

  Parser<Pair<T, U>> then<U>(Parser<U> that) => parserOf(
    (input) {
      var r1 = this.fun(input);
      if(r1.successful) {
        var r2 = that.fun(r1.next);
        return r2.successful ? 
          new ParseSuccess<Pair<T, U>>(new Pair<T, U>(r1.value, r2.value), r2.next) :
          new ParseFailure<Pair<T, U>>(r1.next);
      } else {
        return new ParseFailure<Pair<T, U>>(input);
      }
    }
  );

  Parser<U> map<U>(U f(T result)) => parserOf(
    (input) {
      var r = this.fun(input);
      if(r.successful) {
        return new ParseSuccess<U>(f(r.value), r.next);
      } else {
        return new ParseFailure<U>(r.next);
      }
    }
  );

  Parser<T> chain(Parser<BiFunction<T, T, T> > q) =>
    this.then(q.then(this).many()).map((t) {
      var init = t.item1;
      var list = t.item2;
      return list.fold(init, (a, fb) {
        var f = fb.item1;
        var b = fb.item2;
        return f(a, b);
      });
    });

  Parser<List<T>> many() => parserOf(
    (input) {
      var rest = input;
      List<T> values = [];
      while(true) {
        var r = this.fun(rest);
        if(!r.successful) return new ParseSuccess(values, rest);
        values.add(r.value);
        rest = r.next;
      }
    }
  );

  Parser<List<T>> many1() => this.then(this.many()).map((t) {
    List<T> result = [];
    result.add(t.item1);
    result.addAll(t.item2);
    return result;
  });
}

Parser<String> range(String from, String to) => parserOf((input) {
  var f = from.codeUnitAt(0);
  var t = to.codeUnitAt(0);
  if(input.length < 1) {
    return new ParseFailure<String>(input);
  } else {
    var c = input.codeUnitAt(0);
    if(f <= c && c <= t) {
      return new ParseSuccess<String>(input[0], input.substring(1));
    } else {
      return new ParseFailure<String>(input);
    }
  }
});

Parser<T> rule<T>(Parser<T> body()) => parserOf((input) =>
  body()(input)
);

Parser<T> parserOf<T>(ParserFunction<T> fun) => new Parser<T>(fun);

Parser<String> s(String literal) => parserOf((input) =>
  input.startsWith(literal) ? 
    new ParseSuccess<String>(literal, input.substring(literal.length)) : 
    new ParseFailure<String>(input)
);

使う側はこんな感じです。

import 'dart:core';
import 'parser_combinator.dart';

Parser<int> expression() => rule(
  () => additive()
);

Parser<int> additive() => rule(
  () {
    Parser<BiFunction<int, int, int>> Q = s('+').map((op) => (int lhs, int rhs) => lhs + rhs);
    Parser<BiFunction<int, int, int>> R = s('-').map((op) => (int lhs, int rhs) => lhs - rhs);
    return multitive().chain(Q | R);
  }
);


Parser<int> multitive() => rule(
  () {
    Parser<BiFunction<int, int, int>> Q = s('*').map((op) => (int lhs, int rhs) => lhs * rhs);
    Parser<BiFunction<int, int, int>> R = s('/').map((op) => (int lhs, int rhs) => lhs ~/ rhs);
    return primary().chain(Q | R);
  }
);

Parser<int> primary() => rule(
  () => number() | (s('(').then(expression()).then(s(')'))).map((t) => t.item1.item2)
);

Parser<int> number() => rule(
  () => range('0', '9').many1().map((digits) => int.parse(digits.join()))
);

void main() {
  var e = expression();
  print(e('1+2*(3/4)'));
  print(e('(1+2)*3/4'));
  print(e('10+20*30'));
}
$ dart parser_usage.dart
ParseSuccess(1, )
ParseSuccess(2, )
ParseSuccess(610, )

Hello, Dart (1) - フィボナッチ数列

Flutterを触ってみたくなったので、ついでにDartの勉強も始めることにした。手始めに、フィボナッチ数列を求める Stream を作って、そっから最初の10要素を取り出して表示するというプログラム。

import 'dart:async';
import 'dart:core';

main() async {
  var fibs = await fib().take(10).toList();
  print(fibs);
}

Stream<BigInt> fib() async* {
  var a = new BigInt.from(0);
  var b = new BigInt.from(1);                                                   
  while(true) {
    yield a;
    var c = a + b;
    a = b;
    b = c;
  }
}
$ dart fibs.dart
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Stream#toList() は非同期に処理が完了する Future<List<T>> を返すのだけど、 await で結果を待って List<T> に変換することができるようだ。

ScalaMatsuri 2018 Training Dayで「Scalaに関する神話と真実」という発表をします

このブログを更新するのも、ちょっと久しぶりですね。今回は宣伝というか、表題の件についてちょっと書こうと思います。

ここ数年、ScalaMatsuriはかなりの規模で開催されています。例年は2日間だったのを、3日間に拡張して、最初の1日を 「Training Day」として、主に、Scala初学者やScalaを普段触っていない人向けのプログラムにすることになりました(プログラムはこちらです)。

さて、このTraining Day、結城清太郎さんによる「Scalaハンズオン」、山本裕介さんによる「IntelliJ ハンズオン」などなど、ハンズオン形式のものや入門的な発表が多数あります。Scalaって最近どうなのよという初学者の方や、昔Scalaを触っていたけど最近は触っていないといった人に楽しんでいただけるイベントになると思います。Training Dayのチケットはこちらからお願いします*1。これが1点目。

もう一つは、Training Dayでの自分の発表に関することです。「Scalaに関する神話と真実」と題していますが、要はScalaに関しては色々神話とか誤解(もちろん、真実もありますが)が多数あるので、それを解きほぐすことで、Scalaに対する心理的障壁を下げるのが狙いです。似た発表として、Scala福岡の私の発表「Scalaにまつわる誤解を解く」を思い出された方もいるかもしれません。Scala福岡では比較的概要的な話が多かったですが、ScalaMatsuriでの発表では、より細部に踏み込んだ話をしようかと考えています(時間制約上どこまでできるかわかりませんが)。Scala福岡での発表を聴いた方にもそうでない方にも気づきがあるような発表にしたいのでよろしくお願いします。

例えば:

  • コンパイル時間の問題を緩和するには、実際どうすればいいのか?
  • implicit parameterの利用が適切なとき、不適切なときとはどんなときか?
  • バージョンによる細かな違い

といった点にも触れられればと思います。

というわけで、皆さん、Training Dayのチケットを買って当日お会いしましょう。なお、私は、本編の2日目で「Haskell対Scala」をすることが決まっているので、そちらの方もよろしくお願いします。Haskeller対Scalaistのバトルにならないよう、仲良くできるような発表をしたいと思っています(^^;

*1:Training Dayのチケット*は本編(2日目以降)とは別売りですので注意してください

今年の振り返り

今年もあとわずかとなって来ましたので、今年を振り返ってみようかと思います。

全体的には、対外的なイベントでの発表や主催、あるいは招待が増えたことが今年の特徴かなという感じで、新しいことへの取り組みなどの「挑戦」はあまりしていなかったかなという気がします。

また、これを自分の中でどう位置づけるべきか定められていないのですが、継続して、いくつかの論文の査読やxSIG 2017のPC委員をやったりしています。将来研究職に戻るとしてもこんなんでは全然足りないので(特に論文最近買いてない…)、学会関係のコミットメントはもっとやっていきたいなと思うところです。

その中でも大きかったのは、エフコードさんへの技術顧問の就任かなという気がします(当然ですが所属会社への許可はとっています。兼業申請書さえ出せばいいので、そのへんD社は寛容ですね)。

http://f-code.co.jp/news/170908.html

今までこのように、外側の会社に対して、自分のもっている価値を継続して提供する立場になったことがなかったので、どうすれば、お互いウィンウィンになるのか、日々考えています。

また、地味ですが、自作言語Klassicの開発も進めていました。

https://github.com/klassic/klassic

今年後半は停滞気味でしたが、レコード構文の実装やRow Polymorphismの実装など、欲しい機能がだんだん揃ってきた気がします。来年は、JVMのクラスファイル出力機能を一つのマイルストーンにして、引き続き開発を頑張っていきたいと思います。あと、モチベーション高めるためにも、ロゴが欲しいですね。どなたか協力していただけないでしょうか(丸投げ)。

それでは良いお年を。

Kotlinのコレクションは「不変」と「可変」に分かれていない

Kotlinのコレクションライブラリに関する解説として、よく、

Kotlinのコレクションは不変と可変に分かれていて〜

といった形のものを見かけます。そして、その例として、

val list: List<Int> = listOf(1, 2, 3)
list.add(4) // コンパイルエラー

のようなコードが挙げられていたりします(このような解説を結構見かけるということで、特定の誰かを想定しているわけではありません)。しかし、これは誤りです。以下のコードを実行することで、Kotlinのコレクションの、一見「不変」っぽい型は単に「読み取り専用」の型でしかないことがわかります。

val list1: MutableList<Int> = mutableListOf(1, 2, 3)
val list2: List<Int> = list1
list1[0] = 4
println(list2) // list2が「不変」なら [1, 2, 3] になるはずだが、実際は [4, 2, 3]

最後の行のコメントに說明を書いてありますが、これが全てです。「可変」コレクションを「読み取り専用」の型に代入しても、「可変」コレクションの方を変更してしまうと、その変更は「読み取り専用」の型に代入したコレクションにも波及します。

一見、不変にみえるものが単なる読み取り専用でしかない(ことがある)ので、たとえば、以下のようなコードで

fun setParams(params: List<String>) {
    this.params = params
}

フィールドにうかつに読み取り専用コレクションを代入すると、あとでフィールドの値が変わってしまうことがあります。そのため、場合によっては、防御的コピーを作成する等の手段を講じる必要があるでしょう。

この誤解は本当に頻繁に見かけるので、注意しましょう。

Javaのジェネリクスは「まがい物」ではない

先日、自分が書いた

kmizu.hatenablog.com

に対する反応として、「Javaのようなまがい物のジェネリクスと比較するのは適切でない」「Javaジェネリクスと比較するのは適切でない」(おそらくC#や(C++等(2017/09/24追記))の言語と比較して)といった コメントをいくつか見かけました(はてなブックマークコメントやツイッターなどで)。しかし、ここでは、そのような言説こそが適切でない、ということを言いたいです。なお、methane氏との 対話については既に終わったものなので、それとは関係ありません。

そもそも、Javaジェネリクスは、静的型付き関数型言語で既に一般的であったパラメータ多相をJavaに追加する目的で導入されました(C++テンプレートのようなものをJavaに追加するためだと思っている人がいるかもしれませんが、それは実態にあっていません)。実際、Java Genericsの前身のGeneric Javaのさらに前身であるPizzaの論文で、Martin Odersky*1は、PizzaはJavaにパラメータ多相(parametric polymorphism)などを導入するものであることを明言しています。

この、パラメータ多相という用語は、型理論の分野で広く使われている用語で、型によってコードをパラメータ化できるという性質を指します。重要なのは、パラメータ多相(ジェネリクスと同義)では、そのような、型によってパラメータ化されたコードを表現できて、なおかつその整合性を静的にチェックできるという点が本質であって、 型パラメータが実行時に取得できる とか、 特定の型引数に対して特殊化できる とかいった性質は、あくまで 実装方式 によって得られる追加のメリットであるということです。そして、追加のメリットがないというだけで、Javaジェネリクスが紛い物だとか主張するのは、JavaC#、あるいはJavaC++とかしか知らない人が自らの無理解を示しているに過ぎないし、噴飯モノの言説だということです。

また、実行時に型パラメータ情報を持ったり、特定の型パラメータに関して特殊化できるというのは便利ですが、ランタイムの実装を大幅に複雑化します(実行時にもパラメータ多相を扱えなければいけないので)。一方で、実行時に型パラメータ情報を持たなければ、ランタイムの型システムをある程度無視して言語の型システムを拡張しやすいです(Scalaが典型例)。

別の例としては、Javaは2016年に、型システムが健全でないことが証明されたり型チェックが決定不能であることが証明されたりしていますが、ランタイムの型システムは別なので、これらの問題について、ランタイムが影響を受けることはありません。一方で、C#をはじめとした.NET言語では、言語の型システムとランタイムの型システムがかなり近いため、言語の型システムに欠陥が見つかると、それが直接ランタイムに影響を与えるおそれがあります。

このように、ジェネリクスの実装方式はそれぞれ得失があるので、どちらがいいとすぐ決めつけられるものではありません。両方の性質についてきちんと理解した上で、どちらの方式を支持するか決めるのはもちろんいいですが、その前に色々勉強すべきことがあると思います。

以下、2017/09/24追記

特定の型パラメータに関して特殊化できる、というのの意義が伝わっていなかったので書くと、

class G[T] {
  def puts(x: T) = println(x)
}

があったとして、

G[Int]

に対しては、

class G_Int{
  def puts(x: Int) = println(x)
}

のような効率的なコードが生成される(あるいはできる)ということを指しています。今回、別に実行時型情報のみを問題にしたわけではありません。

Javaのジェネリクスは「まがい物」ではない - kmizuの日記

RTTI(実行時型情報)は何の関係もないです。筆者は根本的なところで勘違いしていませんか?「Scalaジェネリクスだとforがインライン化できずに糞遅い」理由を考えてみましょう。JavaScalaのはただのCastのSugarです。

2017/09/24 10:58
b.hatena.ne.jp

上記の通り、特に勘違いしていません。また、Scalaforが遅い理由についても、何か勘違いされてると思います。キャストの構文糖衣だというような見方については、今回の本文を読んでくださいとしか。言語の型システムの話をしているのに、いきなり、実装としての、「キャストをはさむ」ことに言及されてるので、その二つの違いについてじっくり考えてみられると良いかと。

*1:Scalaの作者であり、GJ Compilerの作者

Re: Go にジェネリクスがなくても構わない人たちに対する批判について

先日自分がGoについてつぶやいたものが、id:methaneさんに捕捉されていて、それに対する反論記事

methane.hatenablog.jp

があがっていたので、それに対する所感を書いてみました。(2017/09/22 追記):cocoatomoさんから指摘があったのですが、引用元は全て id:methane さんの、上記ブログエントリの文章です。雑多な感想に関して反応しているものではないです。

前置き

Goや言語批判に関するスタンスを誤解されると嬉しくないので、最初に書いておきます。

  • Goの言語仕様はあまり好きではないけど、Goユーザーを見下したり、馬鹿だとかそういう風には思っていない

言語仕様批判とユーザー批判は別の話ですが、しばしばいっしょくたにされて、糞言語Xを使うユーザは糞だ、みたいな主張がされることがあるので、 そういう意図はないという表明です。

  • Goユーザーの中で、ジェネリクスがなくても構わないと主張するユーザーへの批判はしたけど、Goユーザー全てがそうだと思っているわけではない

  • Goユーザーの中でジェネリクス不要論を唱えているユーザーへの批判はしたけど、そういうユーザーを馬鹿にしているわけではない

これは掛け値なしの本音なのですが、そう思ってもらえるかどうかについては読み手にお任せするしかありません。

Goはマイクロサービス的なものを開発するための言語か?

Goは特に今で言うマイクロサービス的なものを(色んな意味で)効率よく開発するために作られた言語で、DBとかJSONとかRedisとか扱って HTTP API提供する簡単なサービスを書いてみたら分かると思うんだけど、本当にジェネリクスがなくて不便な場面ってメチャクチャ少ない。 interface {} (Javaでいう object) が出て来る場面って、ジェネリクスが無いせいで出て来ることは本当に稀。

とのことですが、まずこの前提がおかしいのではないかと思います。実際の利用シーンを視ても、コマンドラインツールやWebサービスGCありのC的な使い方、などなど、かなり様々な用途で使われている言語だと思いますし、Goが出たときのドキュメントにも、利用シーンをそれほど限定するような文言はなかったように思います。

ただ、その上でいうと、HTTPとかJSONとかを扱うとき、主にスキーマがないものを扱うため、ジェネリクスがあってもそんなに便利にはならないことが多い、ということはその通りだと思います。DBについては、RDBなら、ジェネリクスを使って、(スキーマの整合性がとれていれば)型安全であることを謳うフレームワークは少なからずあります。

ソート関数とジェネリクスの関係

唯一面倒、あるいは不格好だなぁと思うのはソートだ。ジェネリックなソート関数を使うのが面倒で、便利さのために sort.Ints() とか sort.Strings() とかを提供している*1のが 不格好なんだけど、この面倒臭さも単純にジェネリクスが無いせいとはいうわけでもない。

例えば Javaジェネリクスがプリミティブ型にそのまま対応したとしても、プリミティブ型が .compareTo() を持ってないし演算子オーバーロードとか「この演算子を定義している型」 を宣言する方法を持ってないから、ジェネリック関数に渡すには「値の比較方法」「値の交換方法」を教えてあげないといけなくて面倒だよね。

これは、部分的には正しいのですが、比較関数として、無名関数を使うケース(Java 8以降)だと、ジェネリクスがあるかないかで、比較関数の書きやすさが全く違います。

たとえば、Java 8以降で、整数のリストを降順ソートしたいとき、

Collections.sort(xs, (x, y) -> y.compareTo(x));

と書けば済みますが、ジェネリクスがないと

Collections.sort(xs, (Object x, Object y) -> ((Integer)y).compareTo((Integer)x));

となって面倒さが大幅にあがります。

余談ですが、比較関数を渡すのも面倒だという話はあって、そういう場合に使える言語機構として、Haskellなどにある「型クラス」があります。

Haskellは普段かかないので、Scalaで書くと、次のようになります。

val sortedList = List(3, 2, 4, 1, 5).sorted // 整数同士を比較するオブジェクトが暗黙の内に渡される

でも、Goがジェネリクス持ってないことを非難するときにはJavaを「持ってる」側に分類しておいて、Javaレベルのジェネリクスはあんまり嬉しくない*2ことを説明すると 「ジェネリクスJavaだけの機能じゃない。Javaと比較すんな」みたいな反応されるのにはうんざり。

Javaレベルでのジェネリクスでも上記のように十分うれしいと思いますが、それはともかく、私が言ってないことを勝手に補完してそれに反論されるのはちょっと困惑です。ここに関しては引用がないので、一般論としての反論なのかもしれませんが。

ジェネリクスを使う側と定義する側の嬉しさの混同

一方で今のGoユーザーは、 int v = (int)v.get(i) なんてコードはほぼ書かない。昔のJavaと違って、動的配列もマッピング型もジェネリックな組み込み型だから。たまに書く場合も、 JSONを map[string]interface{} で扱うとか、どんな型でも値として入れられる Context から値を取り出すとか、ジェネリックがあっても便利になるとは思えないケースがほとんどだ。

これは、汎用的な型を「定義」できることに関するメリットを見落としているように思います。Goは確かに動的配列もマップ(連想配列)も組み込み型なので、Javaとは事情が多少違いますが、それ以外のコレクションが欲しくなったときに、Goの組み込み型だけでは対応できません。代表的な例として、集合を表す SetJavaではジェネリックなコレクションとして提供されていますが、現状のGoでそれを実現しようとすると、必然的に要素型は interface{} になってしまって非常に嬉しくありません。

また、コレクションに対して map したり filter したりしてコレクション操作を楽に書けるようになているのが、今時の言語だと一般的だと思いますが(自分もそのように書くのに慣れてしまって、ループでコレクション処理を書かなければいけないのは苦痛です)、このようなジェネリックmap メソッドや filter メソッドを定義しようと思うと、ジェネリックな関数を定義する機能が必要になります。

さらに、標準のコレクションでは性能要求を満たせない場合や、不変コレクションが欲しい場合など、Javaではサードパーティのコレクション(有名なものだとEclipse Collection)を使うという選択肢があって、そのようなサードパーティのコレクションも標準と同じ様な使い勝手を提供できますが、Goではそのようなものを提供しようとすると、要素型がやはり interface{] になってしまいます。

他にも汎用的なグラフライブラリを提供するとき(JGraphT)など、頂点や辺の型は型パラメタとして定義されています。これによって、ユーザ定義のクラスを型安全に、頂点や辺の型として使えます。Goではやはり interface{} になってしまうのも言うまでもないことです。

Goの利用シーンを限定するなら、そのような欠点は大した問題にはならないかもしれませんが、一つの機能がないことで利用シーンを大幅に狭めるのはあまりいいことだとは思えません。

その他

だけど、実際にはジェネリクスなしで型安全を手に入れるために、「イディオム」というコストを払っている。例えばFIFOキューから値を取り出すなら、今は v := q[0]; q = q[1:] なのが、ジェネリクスがあれば v, ok := q.Pop() になるだろう。後者に慣れたら、きっと前者は面倒に感じるはずだ。

イディオム以前に、後者だと間違ったpop操作をする可能性がないのに対して、前者は間違った操作をする可能性があるので、単に面倒以上のコストを払っていると思います。本来なら必要なかった、コードのコピペというコストを払っている、といってもいいです。

頑なに要らないと言ってる人が具体的にどの発言のことを差してるのか分からないけど、コア開発者たちはツールチェインやランタイムの進化を優先していただけで 頑なに拒否してたりはしません。今はツールチェインやランタイムが大分進化したから、Goの適用範囲を広げるためにジェネリクスを含めて機能追加も検討し始めようかっていうフェーズです。

これは私に対する反論ではなく、mizchiさんへの反論ですが、正直この辺は非常に(開発チームの言っていることには)懐疑的です。そもそも、Goが公式に初登場した時期(2009年頃?)でも、ジェネリクスを入れることについては検討中といってたわけで、それから8年経った今でも検討中と言われても一体いつ入れるつもりなんだというのは疑問に思います(その間、いくつものGoにジェネリクスを入れるプロポーザルが登場しているにもかかわらず)。

逆に言語にこだわらない、Goの広い分野への布教に熱心でない人は、ジェネリクスが無いことが不便な場面ではわざわざGoを選ばないでC++C#JavaやRustなどを使ってるので、 本当にジェネリクスが無いことで困ってない。

これはソースが欲しいです。現状、Goの利用シーンはかなり広がっているのに、ジェネリクスが無いことで不便な場面で他の言語にスイッチするというコストを多くのユーザーは払っているのでしょうか?(便利ライブラリやフレームワークの有無が原因ならわかりますが)

全体として、利用シーンをあえて限定した上で、その範囲でならジェネリクスが無くてもあまり困らない、という主張にみえて、Goをそういう言語としてとらえるなら間違ってはいないと思いますが、私はGoをもっと汎用的な言語だと思っているので、その点が噛み合っていないのかなという気がしました。また、上の方でも書きましたが、ジェネリクスによって得られる柔軟性やメリットについてはちょっと過小評価気味ではないかなという気がします。