Go: 提案:Go 2:軽量の無名関数構文

作成日 2017ĺš´08月17日  Âˇ  53コメント  Âˇ  ソース: golang/go

多くの言語は、無名関数を指定するための軽量の構文を提供します。この場合、関数の型は周囲のコンテキストから派生します。

Goツアー(https://tour.golang.org/moretypes/24)の少し工夫された例を考えてみましょう。

func compute(fn func(float64, float64) float64) float64 {
    return fn(3, 4)
}

var _ = compute(func(a, b float64) float64 { return a + b })

多くの言語では、コンテキストから派生している可能性があるため、この場合、無名関数のパラメーターと戻り値の型を削除できます。 例えば:

// Scala
compute((x: Double, y: Double) => x + y)
compute((x, y) => x + y) // Parameter types elided.
compute(_ + _) // Or even shorter.
// Rust
compute(|x: f64, y: f64| -> f64 { x + y })
compute(|x, y| { x + y }) // Parameter and return types elided.

そのようなフォームをGo2に追加することを検討することを提案します。特定の構文を提案することはありません。 言語仕様の観点から、これは、関数型の互換性のある変数に割り当てることができる、型指定されていない関数リテラルの形式と考えることができます。 この形式のリテラルにはデフォルトの型がなく、 x := nilがエラーであるのと同じように、 :=の右側で使用することはできません。

用途1:Cap'n Proto

Cap'n Protoを使用したリモート呼び出しは、入力する要求メッセージが渡される関数パラメーターを取ります。 https://github.com/capnproto/go-capnproto2/wiki/Getting-Startedから:

s.Write(ctx, func(p hashes.Hash_write_Params) error {
  err := p.SetData([]byte("Hello, "))
  return err
})

Rust構文の使用(例として):

s.Write(ctx, |p| {
  err := p.SetData([]byte("Hello, "))
  return err
})

用途2:errgroup

errgroupパッケージ(http://godoc.org/golang.org/x/sync/errgroup)は、ゴルーチンのグループを管理します。

g.Go(func() error {
  // perform work
  return nil
})

Scala構文の使用:

g.Go(() => {
  // perform work
  return nil
})

(この場合、関数のシグネチャは非常に小さいため、これはおそらく、軽量の構文があまり明確でない場合である可能性があります。)

Go2 LanguageChange Proposal

最も参考になるコメント

私はその提案を支持します。 タイピングを節約し、読みやすさを向上させます。私のユースケース、

// Type definitions and functions implementation.
type intSlice []int
func (is intSlice) Filter(f func(int) bool) intSlice { ... }
func (is intSlice) Map(f func(int) int) intSlice { ... }
func (is intSlice) Reduce(f func(int, int) int) int { ...  }
list := []int{...} 
is := intSlice(list)

軽量の無名関数構文なし:

res := is.Map(func(i int)int{return i+1}).Filter(func(i int) bool { return i % 2 == 0 }).
             Reduce(func(a, b int) int { return a + b })

軽量の無名関数構文を使用:

res := is.Map((i) => i+1).Filter((i)=>i % 2 == 0).Reduce((a,b)=>a+b)

全てのコメント53件

私は一般的な考え方に共感していますが、与えられた特定の例はあまり説得力がないことがわかります。構文の点で比較的小さな節約は問題の価値がないようです。 しかし、おそらくもっと良い例やもっと説得力のある表記法があります。

(おそらく二項演算子の例を除いて、しかし、その場合が典型的なGoコードでどれほど一般的であるかはわかりません。)

いいえ、賢いよりも明確な方が良いです。 これらのショートカット構文を見つけました
ありえないほど鈍い。

2017ĺš´8月18日金曜日、04:43 Robert [email protected]
書きました:

私は一般的な考え方に共感していますが、具体的な例を見つけました
あまり説得力がないことを考えると:構文の点で比較的小さな節約
トラブルの価値はないようです。 しかし、おそらくもっと良い例があります
より説得力のある表記。

—
このスレッドにサブスクライブしているため、これを受け取っています。
このメールに直接返信し、GitHubで表示してください
https://github.com/golang/go/issues/21498#issuecomment-323159706 、またはミュート
スレッド
https://github.com/notifications/unsubscribe-auth/AAAcAxlgwt-iPryyY-d5w8GJho0bY9bkks5sZInfgaJpZM4O6pBB
。

関数本体が単純な式である場合に使用を制限すると、これはより説得力があると思います。 ブロックと明示的なreturnを記述する必要がある場合、利点は多少失われます。

あなたの例は

s.Write(ctx, p => p.SetData([]byte("Hello, "))

g.Go(=> nil)

構文は次のようなものです

[ Identifier ] | "(" IdentifierList ")" "=>" ExpressionList

これは、関数型の値への割り当て(関数呼び出しのプロセスでのパラメーターへの割り当てを含む)でのみ使用できます。 識別子の数は関数型のパラメータの数と一致する必要があり、関数型によって識別子の型が決まります。 関数型の結果がゼロであるか、結果パラメーターの数がリスト内の式の数と一致している必要があります。 各式のタイプは、対応する結果パラメーターのタイプに割り当て可能である必要があります。 これは、明らかな方法で関数リテラルと同等です。

ここにはおそらく構文解析のあいまいさがあります。 構文を検討することも興味深いでしょう

Îť [Identifier] | "(" IdentifierList ")" "." ExpressionList

のように

s.Write(ctx, Îťp.p.SetData([]byte("Hello, "))

クロージャーが一般的に使用されるいくつかのケース。

(私は現在、この機能の有用性の証拠を提供するために、主にユースケースを収集しようとしています。)

私は実際、JavaのようにGoが長い無名関数を区別しないのが好きです。

Javaでは、短い無名関数であるラムダは素晴らしく短いのに対し、長い関数は短い関数に比べて冗長で醜いです。 Javaで1行のラムダのみを使用することを推奨するトーク/投稿をどこかで見たことがあります(今は見つかりません)。これらには非冗長性の利点がすべてあるためです。

Goでは、この問題は発生しません。短い匿名関数と長い匿名関数はどちらも比較的(ただしあまり多くはありませんが)冗長であるため、長い関数を使用することに精神的な障害はありません。これは非常に便利な場合があります。

すべてが式であり、関数の結果が関数の定義の最後の式であるため、関数型言語では省略形は自然です。

速記を持っていることは素晴らしいので、上記が当てはまらない他の言語はそれを採用しています。

しかし、私の経験では、ステートメントを使用して言語の現実にぶつかったとき、それは決して素晴らしいことではありません。

ブロックとリターンが必要なため、ほぼ冗長であるか、式のみを含めることができるため、最も単純なものを除いて、基本的には役に立ちません。

Goの無名関数は、最適化できる限り近いものです。 これ以上削ることの価値はわかりません。

問題となるのはfunc構文ではなく、冗長な型宣言です。

関数リテラルが明確な型を削除できるようにするだけで、大いに役立ちます。 Cap'n'Protoの例を使用するには:

s.Write(ctx, func(p) error { return p.SetData([]byte("Hello, ")) })

はい、実際にノイズを追加するのは型宣言です。 残念ながら、「func(p)error」にはすでに意味があります。 おそらく、推論されたタイプの代わりに_を許可することは機能しますか?

s.Write(ctx, func(p _) _ { return p.SetData([]byte("Hello, ")) })

私はむしろそれが好きです。 構文の変更はまったく必要ありません。

_の吃音は好きではありません。 たぶん、funcは、タイプパラメータを推測するキーワードに置き換えることができます。
s.Write(ctx, Îť(p) { return p.SetData([]byte("Hello, ")) })

これは実際に提案ですか、それともハロウィーンのスキームのように服を着せた場合にGoがどのように見えるかを吐き出しているだけですか? この提案は不必要であり、言語が読みやすさに重点を置いていることと一致していないと思います。

他の言語と_見た目_が異なるという理由だけで、言語の構文を変更しようとするのはやめてください。

コールバックベースのAPIに依存する他の言語では、簡潔な無名関数の構文を使用する方が説得力があると思います。 Goでは、新しい構文が実際に効果を発揮するかどうかはわかりません。 人々が無名関数を使用する例がたくさんないわけではありませんが、少なくとも私が読み書きするコードでは、頻度はかなり低いです。

コールバックベースのAPIに依存する他の言語では、簡潔な無名関数の構文を使用する方が説得力があると思います。

ある程度、それは自己強化の条件です。Goで簡潔な関数を作成する方が簡単な場合は、より機能的なスタイルのAPIが表示される可能性があります。 (それが良いことかどうかはわかりません。)

「機能的」APIと「コールバック」APIには違いがあることを強調したいと思います。「コールバック」と聞くと、「非同期コールバック」と思います。これは、幸運にも避けてきた一種のスパゲッティコードにつながります。行け。 同期API( filepath.Walkやstrings.TrimFuncなど)は、一般的にGoプログラムの同期スタイルとのマッチングが優れているため、おそらく私たちが念頭に置いておくべきユースケースです。

ここでチャイムを鳴らして、摩擦を大幅に減らすためのarrowスタイルのラムダ構文(カリー化)を理解するようになったユースケースを提供したいと思います。

検討:

// current syntax
func add(a int) func(int) int {
    return func(b int) int {
        return a + b
    }
}

// arrow version (draft syntax, of course)
add := (a int) => (b int) => a + b

func main() {
    add2 := add(2)
    add3 := add(3)
    fmt.Println(add2(5), add3(6))
}

ここで、値をmongo.FieldConvertFuncまたは機能的なアプローチを必要とするものにカレーしようとしていると想像してください。より軽量な構文を使用すると、関数をカレーされない状態から切り替えるときに、状況が大幅に改善されることがわかります。カレーになります(誰かが望むなら、もっと現実的な例を提供して幸せです)。

納得できませんか? そうは思わなかった。 goのシンプルさも大好きで、保護する価値があると思います。

私によく起こるもう一つの状況は、あなたが持っていて、カレーで次の議論をカレーしたいということです。

今、あなたは変更する必要があります
func (a, b) x
き
func (a) func(b) x { return func (b) { return ...... x } }

矢印構文がある場合は、単に変更します
(a, b) => x
き
(a) => (b) => x

@neild私はまだこのスレッドに貢献していませんが、あなたが提案したものと同様の何かから利益を得る別のユースケースがあります。

しかし、このコメントは、実際には、コードを呼び出す際の冗長性に対処する別の方法に関するものですgocode (または同様の)テンプレートのようなツールを使用して、関数値を取得します。

あなたの例を取る:

func compute(fn func(float64, float64) float64) float64 {
    return fn(3, 4)
}

入力したと仮定すると、次のようになります。

var _ = compute(
                ^

^示される位置にカーソルを置きます。 次に、そのようなツールを呼び出すと、次のような関数値を簡単にテンプレート化できます。

var _ = compute(func(a, b float64) float64 { })
                                            ^

それは確かに私が考えていたユースケースをカバーするでしょう。 それはあなたをカバーしていますか?

コードは、書かれているよりもはるかに頻繁に読み取られます。 ここで言語構文を変更する価値があるとは思いません。 利点がある場合、それは主にコードをより読みやすくすることです。 エディターのサポートはそれを助けません。

もちろん、問題は、無名関数から完全な型の情報を削除することが読みやすさを助けるか害するかということです。

この種の構文が読みやすさを低下させるとは思いません。ほとんどすべての最新のプログラミング言語には、関数型スタイルを使用してボイラープレートを減らし、コードをより明確で保守しやすくするための構文があります。 関数にパラメーターとして渡されるときにgolangで無名関数を使用するのは非常に苦痛です。これは、渡さなければならないことがわかっている型をもう一度入力する必要があるためです。

私はその提案を支持します。 タイピングを節約し、読みやすさを向上させます。私のユースケース、

// Type definitions and functions implementation.
type intSlice []int
func (is intSlice) Filter(f func(int) bool) intSlice { ... }
func (is intSlice) Map(f func(int) int) intSlice { ... }
func (is intSlice) Reduce(f func(int, int) int) int { ...  }
list := []int{...} 
is := intSlice(list)

軽量の無名関数構文なし:

res := is.Map(func(i int)int{return i+1}).Filter(func(i int) bool { return i % 2 == 0 }).
             Reduce(func(a, b int) int { return a + b })

軽量の無名関数構文を使用:

res := is.Map((i) => i+1).Filter((i)=>i % 2 == 0).Reduce((a,b)=>a+b)

簡潔な無名関数式がないため、Goは読みにくくなり、DRYの原則に違反します。 関数型/コールバックAPIを記述して使用したいのですが、そのようなAPIの使用は、すべてのAPI呼び出しで、定義済みの関数またはコンテキストから非常に明確な型情報を繰り返す匿名関数式のいずれかを使用する必要があるため、非常に冗長です( APIは正しく設計されています)。

この提案に対する私の願望は、Goが他の言語のように見えたり、似ているべきだと私が思うほど遠くはありません。 私の欲求は、自分自身を繰り返し、不必要な構文上のノイズを含めることへの嫌悪感に完全に駆り立てられています。

Goでは、関数宣言の構文は、他の宣言の通常のパターンから少し外れています。 定数、型、変数については、常に次のものがあります。

keyword name type value

例えば:

const   c    int  = 0
type    t    foo
var     v    bool = true

一般に、型はリテラル型にすることも、名前にすることもできます。 これが機能する関数の場合、型は常にリテラル署名である必要があります。 次のような画像を作成できます。

type BinaryOp func(x, y Value) Value

func f BinaryOp { ... }

ここで、関数型は名前として指定されます。 少し拡張すると、BinaryOpクロージャはおそらく次のように書くことができます

BinaryOp{ return x.Add(y) }

これは、クロージャー表記を短くするのに大いに役立つ可能性があります。 例えば:

vector.Apply(BinaryOp{ return x.Add(y) })

主な欠点は、パラメーター名が関数で宣言されていないことです。 関数型を使用すると、それらは「スコープ内」になります。これは、タイプSの構造体値xを使用すると、セレクター式x.fのスコープにフィールドfが入るのと同じです。 x.fまたは構造体リテラルS{f: "foo"} 。

また、これには明示的に宣言された関数型が必要です。これは、その型が非常に一般的である場合にのみ意味があります。

この議論のちょうど別の視点。

読みやすさが第一であり、それは私たち全員が同意できることのようです。

とはいえ、私がチャイムを鳴らしたいのは(他の誰もが明示的に言っているようには見えないので)、読みやすさの問題は常にあなたが慣れていることにかかっているということです。 読みやすさを損なうのか害にするのかについて話し合うことは、私の意見ではどこにも行きません。

@griesemerおそらく、V8に取り組んでいる時間からのいくつかの視点がここで役立つでしょう。 私は(少なくとも)関数( function(x) { return x; } )のjavascriptの以前の構文に非常に満足していたと言えます。これは、(ある意味で)現在のGoよりもさらに読みやすくなっています。 私は@douglascrockfordの「この新しい構文は時間の無駄です」キャンプにいました。

しかし、それでも、矢印の構文は_happened_であり、私はそれを_しなければならなかったので_受け入れました。 しかし、今日では、それをより多く使用し、より快適になったことで、読みやすさが大幅に向上したと言えます。 私はカリー化のケースを使用しました(そして@hooluupogは「ドットチェーン」の同様のケースを提起しました)。軽量の構文は過度に巧妙でなくても軽量のコードを生成します。

今、 x => y => z => ...のようなことをするコードを見ると、一目で理解するのがはるかに簡単になります(繰り返しますが、私はそれに慣れているので、それほど昔ではありませんでしたが、まったく逆のことを感じました)。

私が言っているのは、この議論は次のように要約されます。

  1. あなたがそれに慣れていないとき、それは読みやすさに害がないにしても、_本当に_奇妙で境界線は役に立たないようです。 一部の人々は、これについて何らかの形で感情を持っているか、持っていないだけです。
  2. 実行している関数型プログラミングが多ければ多いほど、そのような構文の必要性はそれ自体を発音します。 これは、読者のノイズにつながる小さな仕事に多くの機能を導入する機能概念(部分適用やカリー化など)と関係があると思います。

私たちができる最善のことは、より多くのユースケースを提供することです。

@dimitropoulosのコメントに応えて、これが私の見解の大まかな要約です。

現在の構文での使用は非常に冗長であるため、この提案から大きなメリットが得られるデザインパターン(関数型プログラミングなど)を使用したいと思います。

@dimitropoulos私はV8に取り組んできましたが、それはC ++で記述された仮想マシンを構築することでした。 実際のJavascriptでの私の経験は限られています。 とは言うものの、Javascriptは動的に型付けされた言語であり、型がなければ型付けの多くはなくなります。 何人かの人が以前に提起したように、ここでの主要な問題は、型を繰り返す必要があることです。これは、Javascriptには存在しない問題です。

また、記録のために:Goの設計の初期の頃、私たちは実際に関数シグネチャの矢印構文を調べました。 詳細は覚えていませんが、

func f (x int) -> float32

ホワイトボードにありました。 複数の(タプルではない)戻り値ではうまく機能しなかったため、最終的に矢印を削除しました。 funcとパラメータが存在する場合、矢印は不要でした。 おそらく「きれい」(数学的に見えるように)ですが、それでも不要です。 また、「異なる」種類の言語に属する構文のようにも見えました。

しかし、パフォーマンスの高い汎用言語でクロージャを使用することで、新しい、より機能的なプログラミングスタイルへの扉が開かれました。 10年後の今、別の角度から見るかもしれません。

それでも、クロージャー用の特別な構文を作成しないように、ここでは非常に注意する必要があると思います。 私たちが今持っているものはシンプルで定期的であり、これまでうまく機能してきました。 アプローチがどうであれ、何か変化があれば、それは定期的であり、あらゆる機能に適用される必要があると私は信じています。

Goでは、関数宣言の構文は、他の宣言の通常のパターンから少し外れています。 定数、型、変数については、常に次のものがあります。
keyword name type value
[…]
これが機能する関数の場合、型は常にリテラル署名である必要があります。

パラメータリストとconstおよびvar宣言の場合、同様のパターンIdentifierList Typeがあり、これもおそらく保持する必要があることに注意してください。 ラムダ計算スタイルの:トークンを除外して、変数名を型から分離するようです。

アプローチがどうであれ、何か変化があれば、それは定期的であり、あらゆる機能に適用される必要があると私は信じています。

keyword name type valueパターンは_declarations_用ですが、 @ neildが言及するユースケースはすべて_literals_用です。

リテラルの問題に取り組むと、宣言の問題は取るに足らないものになると思います。 定数、変数、およびnow型の宣言では、 valueの前に=トークンを許可(または要求)します。 それを関数に拡張するのは簡単なようです。

FunctionDecl = "func" ( FunctionSpec | "(" { FunctionSpec ";" } ")" ).
FunctionSpec = FunctionName Function |
               IdentifierList (Signature | [ Signature ] "=" Expression) .

FunctionLit = "func" Function | ShortFunctionLit .
ShortParameterList = ShortParameterDecl { "," ShortParameterDecl } .
ShortParameterDecl = IdentifierList [ "..." ] [ Type ] .

=トークンの後の式は、関数リテラルであるか、コンパイル時に引数がすべて使用可能な呼び出しによって返される関数である必要があります。 =形式でも、引数型の宣言をリテラルからFunctionSpecに移動するために、 Signatureを指定できます。

ShortParameterDeclと既存のParameterDeclの違いは、シングルトンIdentifierListが型ではなくパラメーター名として解釈されることです。


例

今日受け入れられたこの関数宣言を考えてみましょう。

func compute(f func(x, y float64) float64) float64 { return f(3, 4) }

以下の例に加えて、それを保持するか(Go 1互換性など)、 Functionの本番環境を削除して、 ShortFunctionLitバージョンのみを使用することができます。

さまざまなShortFunctionLitオプションについて、上記で提案した文法は次のようになります。

さびのような:

ShortFunctionLit = "|" ShortParameterList "|" Block .

次のいずれかを認めます。

func compute = |f func(x, y float64) float64| { f(3, 4) }
func compute(func (x, y float64) float64) float64 = |f| { f(3, 4) }



md5-c712da47cbcf3d0379ff810dfd76ce59



```go
func (
    compute(func (x, y float64) float64) float64 = |f| { f(3, 4) }
)



md5-8a4d86e5ac5f718d8d35839eaf9f1029



ShortFunctionLit = "(" ShortParameterList ")" "=>" Expression .



md5-e429c4db0e2a76fe83f1f524910c0075



```go
func compute(func (x, y float64) float64) float64 = (f) => f(3, 4)



md5-bcb7677c087284f6121b65ce14d46d93



```go
func (
    compute(func (x, y float64) float64) float64 = (f) => f(3, 4)
)



md5-bf0cf8ca5f55bbedf92dc2047d871378



ShortFunctionLit = "Îť" ShortParameterList "." Expression .



md5-3c1a0d273a1aee09721883f5be8fcfce



```go
func compute(func (x, y float64) float64) float64) = Îťf.f(3, 4)



md5-87735958588cf5a763da8a89d1f9a675



```go
func (
    compute(func (x, y float64) float64) float64) = Îťf.f(3, 4)
)



md5-d613a37ac429244205560535e5401d63



ShortFunctionLit = "\" ShortParameterList "->" Expression .



md5-95523002741f1036dff7837c1701336d



```go
func compute(func (x, y float64) float64) float64) = \f -> f(3, 4)



md5-818e7097669fe3bc7a333787735e5657



```go
func (
    compute(func (x, y float64) float64) float64) = \f -> f(3, 4)
)



md5-af63df358fad8d4beffd23e2d0c337a4



ShortFunctionLit = "[" ShortParameterList "]" Block .



md5-f66b9b33e7dca8cce60726de14cfc931



```go
func compute(func (x, y float64) float64) float64) = [f] { f(3, 4) }



md5-13e2e0ab357ce95a5a0e2fbd930ba841



```go
func (
    compute(func (x, y float64) float64) float64) = [f] { f(3, 4) }
)

個人的には、Scalaのようなバリアントを除いてすべてがかなり読みやすいと思います。 (私の目には、Scalaのようなバリアントは括弧が重すぎます。これにより、線のスキャンがはるかに困難になります。)

個人的には、推測できるときにパラメーターと結果のタイプを省略できるのであれば、主にこれに興味があります。 それができれば、現在の関数リテラル構文でも問題ありません。 (これについては上で説明しました。)

確かに、これは@griesemerのコメントに反します。

アプローチがどうであれ、何か変化があれば、それは定期的であり、あらゆる機能に適用される必要があると私は信じています。

私はこれに完全には従いません。 関数本体から十分な精度で関数を導出する方法がないため、関数宣言には必ず関数の完全な型情報を含める必要があります。 (もちろん、これはすべての言語に当てはまるわけではありませんが、Goには当てはまります。)

対照的に、関数リテラルはコンテキストから型情報を推測できます。

@neild不正確なことをお詫びします:私がこの文を意味したのは、新しい異なる構文(矢印またはあなたが持っているもの)があった場合、それはやや規則的でどこにでも適用されるはずだということです。 タイプを省略できる可能性がある場合、それは再び直交します。

@griesemerありがとう; 私は(ほとんど)その点に同意します。

この提案の興味深い質問は、構文を使用することが良いアイデアかどうかということだと思います。 その構文が何であるかは重要ですが、比較的簡単です。

しかし、私は自分の提案を少し自転車に乗せたいという誘惑に抵抗することはできません。

var sum func(int, int) int = func a, b { return a + b }

@neildの提案は私にとって正しいと感じています。 これは既存の構文にかなり近いですが、型指定の繰り返しを排除するため、関数型プログラミングで機能します。 (a, b) => a + bよりもはるかにコンパクトではなく、既存の構文にうまく適合します。

@neild

var sum func(int, int) int = func a, b { return a + b }

それは変数または関数を宣言しますか? 変数の場合、同等の関数宣言はどのようになりますか?

上記の宣言スキーマでは、正しく理解している場合は次のようになります。

ShortFunctionLit = "func" ShortParameterList Block .
func compute = func f func(x, y float64) float64 { return f(3, 4) }
func compute(func (x, y float64) float64) float64 = func f { return f(3, 4) }
func (
    compute = func f func(x, y float64) float64 { return f(3, 4) }
)
func (
    compute(func (x, y float64) float64) float64 = func f { return f(3, 4) }
)

私はファンではないと思います。 funcで少し途切れ、 funcトークンとそれに続くパラメーターの間に十分な視覚的ブレークを提供していないようです。

または、リテラルに割り当てるのではなく、宣言から括弧を除外しますか?

func compute f func(x, y float64) float64 { return f(3, 4) }

私はまだ視覚的な休憩の欠如が好きではありませんが...

それは変数または関数を宣言しますか? 変数の場合、同等の関数宣言はどのようになりますか?

変数。 同等の関数宣言はおそらくfunc sum a, b { return a+b }ですが、明らかな理由で無効になります。関数宣言からパラメーター型を削除することはできません。

私が考えている文法の変更は次のようになります。

ShortFunctionLit = "func" [ IdentifierList ] [ "..." ] FunctionBody .

短い関数リテラルは、パラメーターリストの括弧を省略して通常の関数リテラルと区別され、着信パラメーターの名前のみを定義し、発信パラメーターは定義しません。 着信パラメータのタイプと発信パラメータのタイプおよび数は、周囲のコンテキストから導出されます。

短い関数リテラルでオプションのパラメーター型を指定できるようにする必要はないと思います。 その場合は、通常の関数リテラルを使用するだけです。

@ianlancetaylorが指摘したように、軽量表記は、パラメータータイプを簡単に推測できるため、パラメータータイプの省略が許可されている場合にのみ意味があります。 そのため、 @ neildの提案は、私がこれまでに見た中で最高かつ最も単純なものです。 ただし、簡単に許可されないことの1つは、名前付き結果パラメーターを参照する関数リテラルの軽量表記です。 しかし、おそらくその場合、彼らは完全な表記法を使用する必要があります。 (少し不規則です)。

(x, y) { ... }をfunc (x, y T) T { ... }の短縮形として解析できる場合もあります。 パーサーの先読みが少し必要になりますが、それほど悪くはないかもしれません。

実験として、関数リテラルをコンパクトな構文に書き直し、src /に対して実行するようにgofmtを変更しました。 ここで結果を見ることができます:

https://github.com/neild/go/commit/2ff18c6352788aa8f8cbe8b5d5d4c73956ca7c6f

私はこれを理にかなっている場合に限定しようとはしませんでした。 コンパクトな構文が実際にどのように機能するかを理解したかっただけです。 結果について意見を述べるには、まだ十分に掘り下げていません。

@neildニース分析。 いくつかの観察:

  1. 明示的な型注釈なしでこれらのケースを処理するには、より複雑な推論アルゴリズムが必要になるため、関数リテラルが:=を使用してバインドされるケースの割合は期待外れです。

  2. コールバックに渡されるリテラルは、読みやすい場合もあれば、読みにくい場合もあります。
    たとえば、多くの行にまたがる関数リテラルのreturn-type情報を失うことは、読者に機能APIを探しているのか、命令型APIを探しているのかを知らせるため、少し残念です。

  3. スライス内の関数リテラルの定型文の削減は大幅です。

  4. deferおよびgoステートメントは興味深いケースです。実際に関数に渡された引数から引数の型を推測しますか?

  5. 末尾の...トークンのいくつかが例から欠落しています。

deferとgoは確かに非常に興味深いケースです。

go func p {
  // do something with p
}("parameter")

実際の関数パラメーターからpのタイプを導き出しますか? これはgoステートメントには非常に便利ですが、もちろん、クロージャーを使用するだけでほぼ同じ効果を得ることができます。

p := "parameter"
go func() {
  // do something with p
}()

私はこれを完全にサポートします。 率直に言って、「他の言語のように見える」かどうかは気にせず、匿名関数を使用するためのより冗長でない方法が必要です。

編集:複合リテラル構文を借用しています...

type F func(int) float64
var f F
f = F {      (i) (o) { o = float64(i); return } }
f = F {      (i) o   { o = float64(i); return } } // single return value
f = F { func (i) o   { o = float64(i); return } } // +func for good measure?

ただのアイデア:
Swiftの構文を使用した_untyped関数literal_を使用したOPの例は次のようになります。

compute({ $0 + $1 })

これには、Go1と完全な下位互換性があるという利点があると思います。

簡単なtcpチャットアプリを書いていたので、これを見つけました。
基本的に私はその中にスライスがある構造を持っています

type connIndex struct {
    conns []net.Conn
    mu    sync.Mutex
}

同時にいくつかの操作を適用したい(接続の追加、すべての人へのメッセージの送信など)

ミューテックスのロックコードをコピーして貼り付けるという通常のパスをたどったり、デーモンのゴルーチンを使用してアクセスを管理したりする代わりに、クロージャーを渡すだけだと思いました

func (c *connIndex) run(f func([]net.Conn)) {
    c.mu.Lock()
    defer c.mu.Unlock()
    f(c.conns)
}

短い操作の場合、その過度に冗長です( lockおよびdefer unlock()よりも優れています)

conns.run(func(conns []net.Conn) { conns = append(conns, conn) })

runメソッドで正確な関数シグネチャを入力したため、これはDRYの原則に違反します。

関数シグネチャの推測がサポートされている場合は、次のように記述できます

conns.run(func(conns) { conns = append(conns, conn) })

これによってコードが読みにくくなるとは思いません。 appendのせいでスライスであることがわかります。また、変数に適切な名前を付けているので、見なくても[] net.Connであると推測できます。 runメソッドシグネチャで。

関数本体に基づいてパラメーターのタイプを推測しようとするのは避け、代わりに、それが明らかな場合(関数にクロージャを渡すなど)にのみ推論を追加します。

読者にオプションを提供するので、これは読みやすさを損なうことはないと思います。パラメーターのタイプがわからない場合は、 godefするか、カーソルを合わせてエディターに表示させることができます。 。

それを表示/ジャンプするボタンがあることを除いて、本の中で彼らがキャラクターの紹介を繰り返さない方法のように並べ替えます。

私は書くのが苦手なので、うまくいけば、これを読んで生き残ったでしょう:)

関数本体が単純な式である場合に使用を制限すると、これはより説得力があると思います。

私はあえて反対します。 これでも関数を定義する2つの方法につながります。私が囲碁に恋をした理由のひとつは、あちこちで冗長性がありながら、さわやかな表現力があることです。トレースすると、 funcキーワードまたはパラメーターのいずれかが関数になります。

conns.run(func(conns []net.Conn) { conns = append(conns, conn) })
runメソッドに正確な関数シグネチャを入力したため、これはDRYの原則に違反します。

間違いなく、DRYは重要です。 しかし、可能な限り最小限の労力でコードを理解する能力を犠牲にして原則を維持するためにプログラミングのすべての部分にそれを適用することは、マーク、imhoを少しオーバーシュートしています。

ここでの一般的な問題(および他のいくつかの提案)は、議論が主にコードを安全に作業する方法についてであるのに対し、コードを安全に作業する方法についてであると思います。 それを書いた数年後。 私は最近poc.plに来ましたが、それが何をするのかまだ理解しようとしています...;)

conns.run(func(conns) { conns = append(conns, conn) })
これによってコードが読みにくくなるとは思いません。追加のためにスライスであることがわかります。変数に名前を付けたので、実行メソッドのシグネチャを見なくても[] net.Connであると推測できます。 。

私の見解では、この声明にはいくつかの問題があります。 他の人がそれをどのように見ているかはわかりませんが、私は推測するのが嫌いです。 1つは正しいかもしれませんし、1つは間違っているかもしれませんが、確かにそれに努力する必要があります-「タイプ」 []net.Connに保存するために。 また、コードの可読性とわかりやすさは、それに基づくのではなく、適切な変数名によってサポートされる必要があります。

結論として:議論の焦点は、コードを書くときの小さな労力を減らす方法から、そのコードを理解するための労力を減らす方法に移るべきだと思います。

最後に、DaveCheneyがRobertPike(iirc)を引用して締めくくります。

クリアは賢いよりも優れています。

関数の署名を入力する面倒な作業は、オートコンプリートによっていくらか軽減できます。 たとえば、goplsは、関数リテラルを作成する補完を提供します。
cb

これは、型名がまだソースコードにあり、無名関数を定義する方法が1つしかなく、署名全体を入力する必要がないという良い中間点を提供すると思います。

これは追加されますか?
...この機能が気に入らない場合でも、古い構文を使用できます。
...よりシンプルにしたい私たちにとって、この新機能をうまく利用できます。私がgoを書いてから1年が経ちましたが、コミュニティがこれが重要であるとまだ考えているかどうかはわかりません。
...これは追加されますか?

@noypi決定はされていません。 この問題は未解決のままです。

https://golang.org/wiki/NoPlusOne

私はこの提案を支持し、この機能をジェネリックスと組み合わせることで、Goの関数型プログラミングが開発者にとってより使いやすくなると思います。

これが私が見たいものです、大まかに:

type F func(int, int) int

// function declaration
f := F (x, y) { return x * y}

// function passing 
// g :: func(F)
g((x, y) { return x * y })

// returning function
func h() F {
    return (x, y) { return x * y }
}

(a, b) => a * bと入力して、次に進むことができるようにしたいと思います。

私は矢印機能がまだGolangで利用できないと信じることができません。
Javascriptでの作業がいかに明確でシンプルかは驚くべきことです。

JavaScriptは、実際に使用されるまでパラメーター、パラメーターの数、値、またはタイプを気にしないため、これを簡単に実装できます。

関数リテラルの型を省略できると、GioレイアウトAPIで使用する関数スタイルに大いに役立ちます。 https://git.sr.ht/~eliasnaur/gio/tree/master/example/kitchen/kitchen.go?にある多くの「func(){...}」リテラルを参照してください。 彼らの実際の署名は次のようなものでなければなりませんでした

func(gtx layout.Context) layout.Dimensions

ただし、型名が長いため、 gtxは、各関数呼び出しからの着信値と発信値を含む共有layout.Contextへのポインターです。

明確さと正確さのために、この問題に関係なく、おそらく長い署名に切り替えるつもりです。 それにもかかわらず、私の場合は、より短い関数リテラルをサポートする優れた経験レポートであると思います。

PS私が長い署名に傾倒している理由の1つは、タイプエイリアスによって署名を短縮できるためです。

type C = layout.Context
type D = layout.Dimensions

これにより、リテラルがfunc(gtx C) D { ... }に短縮されます。

2番目の理由は、長い署名がこの問題を解決するものと上位互換性があることです。

私はアイデアを持ってここに来ましたが、 @ networkimprovがすでにここに似たようなものを提案していることがわかりました。

関数リテラルの指定子として関数型(名前のない関数型またはエイリアスの場合もあります)を使用するというアイデアが好きです。これは、パラメーターと戻り値に通常の型推論ルールを使用できることを意味します。事前に正確なタイプ。 これは、(たとえば)オートコンプリートが通常どおり機能し、ファンキーなトップダウン型推論規則を導入する必要がないことを意味します。

与えられた:

type F func(a, b int) int

私の最初の考えは:

F(a, b){return a + b}

しかし、それは通常の関数呼び出しに非常に似ています- aとbがそこで定義されているようには見えません。

他の可能性を捨てる(私はそれらのどれも特に好きではありません):

F->(a, b){return a + b}
F::(a, b){return a + b}
(a, b := F){ return a + b }
F{a, b}{return a + b}
F{a, b: return a + b}
F{a, b; return a + b}

おそらく、このあたりのどこかにいくつかの素晴らしい構文が潜んでいます:)

複合リテラル構文の重要なポイントは、パーサーに型情報を必要としないことです。 構造体、配列、スライス、およびマップの構文は同じです。 パーサーは、 T{...}の構文ツリーを生成するために、 Tのタイプを知る必要はありません。

もう1つのポイントは、構文もパーサーでのバックトラックを必要としないことです。 {が複合リテラルの一部であるかブロックの一部であるかがあいまいな場合、そのあいまいさは常に後者を優先して解決されます。

funcキーワードを保持することでパーサーのあいまいさを回避する、この号の前半で提案した構文がまだ好きです。

func a, b { return a + b }

:-1:を削除しました。 私はまだ:+ 1:ではありませんが、自分の立場を再考しています。 ジェネリックスはgenericSorter(slice, func(a, b T) bool { return a > b })のような短い関数の増加を引き起こします。 また、 https: //github.com/golang/go/issues/37739#issuecomment-624338848が魅力的であることがわかりました。

関数リテラルをより簡潔にするために議論されている2つの主要な方法があります。

  1. 式を返すボディの短縮形
  2. 関数リテラルの型を削除します。

両方を別々に扱うべきだと思います。

FunctionBodyが次のように変更された場合

FunctionBody = Block | "->" ExpressionBody
ExpressionBody = Expression | "(" ExpressionList ")"

これは主に、型の省略の有無にかかわらず関数リテラルを支援し、ページ上で非常に単純な関数とメソッドの宣言を軽くすることもできます。

func (*T) Close() error -> nil

func (e *myErr) Unwrap() error -> e.err

func Alias(x int) -> anotherPackage.OriginalFunc(x)

func Id(type T)(x T) T -> x

func Swap(type T)(x, y T) -> (y, x)

(godocと友達はまだ体を隠すことができます)

その例では@ianlancetaylorの構文を使用しましたが、その主な欠点は、新しいトークン(および、 func(c chan T) -> <-cでは奇妙に見えるトークン)を導入する必要があることですが、あいまいさがない場合は、「=」などの既存のトークンを再利用します。 この投稿の残りの部分では「=」を使用します。

タイプエリジオンの場合、2つのケースがあります

  1. 常に機能するもの
  2. タイプを推測できるコンテキストでのみ機能するもの

@griesemerが提案するような名前付きタイプを使用すると、常に機能します。 構文に問題があるようです。 私はそれがうまくいくと確信しています。 たとえそうだったとしても、それが問題を解決するかどうかはわかりません。 名前付きタイプの急増が必要になります。 これらは、それらが使用される場所を定義するパッケージに含まれるか、それらを使用するすべてのパッケージで定義される必要があります。

前者では、次のようなものが得られます

slices.Map(s, slices.MapFunc(x) = math.Abs(x-y))

後者では、次のようなものが得られます

type mf func(float64) float64
slices.Map(s, mf(x) = math.Abs(x-y))

いずれにせよ、それぞれの名前が頻繁に使用されない限り、ボイラープレートをあまり削減しないほどの混乱があります。

@neildのような構文は、型を推測できる場合にのみ使用できます。 簡単な方法は、#12854のように、タイプがわかっているすべてのコンテキスト(関数へのパラメーター、フィールドへの割り当て、チャネルでの送信など)をリストするだけです。 @neildが提起したgo / deferケースも含めると便利なようです。

そのアプローチでは、具体的に次のことはできません

zero := func = 0
var f interface{} = func x, y = g(y, x)

しかし、それらがどこでどのように使用されているかを調べることによってアルゴリズム的にタイプを推測することができたとしても、それらはより明確にすることが有益である場合です。

それは、最も有用な/要求されたものを含む多くの有用なケースを可能にします:

slices.Map(s, func x = math.Abs(x-y))
v := cond(useTls, FetchCertificate, func = nil)

リテラル構文に関係なくブロックを使用することを選択できることにより、次のことも可能になります。

http.HandleFunc("/bar", func w, r {
  // many lines ...
})

これは、私を:+ 1:に向けてますます押し進める特定のケースです。

私が提起されたのを見たことがない1つの質問は、 ...パラメーターをどのように処理するかです。 あなたはどちらかについて議論することができます

f(func x, p = len(p))
f(func x, ...p = len(p))

私にはその答えがありません。

@jimmyfrasche

  1. 関数リテラルの型を削除します。

これは、関数型リテラルを追加して処理する必要があると思います。 タイプが 'func'に置き換わり、引数タイプが発行される場合(タイプによって定義されるため)。 これにより、読み取り可能性が維持され、他のタイプのリテラルとかなり一致します。

http.Handle("/", http.HandlerFunc[w, r]{
    fmt.Fprinf(w, "Hello World")
})
  1. 式を返すボディの短縮形

関数を独自のタイプとしてリファクタリングすると、物事がはるかにクリーンになります。

type ComputeFunc func(float64, float64) float64

func compute(fn ComputeFunc) float64 {
    return fn(3, 4)
}

compute(ComputeFunc[a,b]{return a + b})

これが冗長すぎる場合は、コード内で関数型のエイリアスを入力します。

{
    type f = ComputeFunc

    compute(f[a,b]{return a + b})
}

引数のない関数の特殊なケースでは、角かっこは省略してください。

type IntReturner func() int

fmt.Println(IntReturner{return 2}())

契約提案ではすでにジェネリック関数に追加の標準ブラケットが使用されているため、角括弧を選択します。

@Splizard私は、それがリテラル構文から多くの追加の型定義に混乱を押しやるだけだという議論を支持します。 このような各定義は、リテラルに型を書き込むよりも短くなる前に、少なくとも2回使用する必要があります。

また、すべての場合にジェネリックとうまく機能するかどうかもわかりません。

かなり奇妙な関数を考えてみましょう

func X(type T)(v T, func() T)

Xで使用するジェネリック型に名前を付けることができます:

type XFunc(type T) func() T

XFuncの定義のみを使用してパラメーターのタイプを導出する場合、 Xを呼び出すときに、使用するTを指定する必要があります。 vのタイプ:

X(v, XFunc(T)[] { /* ... */ })

このようなシナリオでは、 Tを推測できるようにする特殊なケースがありますが、その場合、funcリテラルの型の省略に必要となる多くの機械が必要になります。

X Tごとに新しいタイプを定義することもできますが、 Tごとに何度もXを呼び出さない限り、それほど節約にはなりません。 。

このページは役に立ちましたか?
0 / 5 - 0 評価