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.

我建议考虑在 Go 2 中添加这样的形式。我没有提出任何特定的语法。 就语言规范而言,这可以被认为是一种无类型函数字面量的形式,可分配给任何兼容的函数类型变量。 这种形式的文字没有默认类型,不能用在:=的右侧,就像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
})

用途二:errgroup

errgroup 包(http://godoc.org/golang.org/x/sync/errgroup)管理一组 goroutine:

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 Griesemer [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, "))

还有一些常用闭包的情况。

(我目前主要是尝试收集用例,以提供支持/反对此功能实用性的证据。)

我实际上喜欢 Go 不像 Java 那样区分更长的匿名函数。

在 Java 中,一个简短的匿名函数 lambda 既漂亮又简短,而与较短的函数相比,较长的匿名函数既冗长又丑陋。 我什至在某处看到过一篇演讲/帖子(我现在找不到它),它鼓励在 Java 中只使用单行 lambda,因为它们具有所有这些非冗长的优势。

在 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(例如filepath.Walkstrings.TrimFunc )可能是我们应该考虑的用例,因为它们通常与 Go 程序的同步风格更好地结合。

我只想在这里插话并提供一个用例,我已经开始欣赏arrow样式的 lambda 语法以大大减少摩擦:currying。

考虑:

// 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) })

主要缺点是参数名称未与函数一起声明。 使用函数类型将它们带入“范围内”,类似于使用 $#$6$ x S将字段f带入选择器表达式x.f的范围内S{f: "foo"}

此外,这需要显式声明的函数类型,这可能仅在该类型非常常见时才有意义。

只是这个讨论的另一个角度。

可读性是第一位的,这似乎是我们都可以同意的。

但话虽如此,我还想插话的一件事(因为它看起来不像其他人明确说过的那样)是可读性问题总是取决于你习惯了什么。 在我看来,就它是否会损害或损害可读性进行讨论不会有任何进展。

@griesemer也许您在 V8 上工作时的一些观点在这里会很有用。 我(至少)可以说我对 javascript 先前的函数语法( function(x) { return x; } )非常满意,它(在某种程度上)比现在的 Go 更难阅读。 我参加了@douglascrockford的“这种新语法是浪费时间”阵营。

但是,箭头语法_happened_和我都接受了_因为我不得不_。 不过,今天,我已经更多地使用它并且对它更加熟悉了,我可以说它极大地提高了可读性。 我使用了柯里化的案例( @hooluupog提出了类似的“点链”案例),其中轻量级语法生成的代码既轻量级又不会过于聪明。

现在,当我看到执行x => y => z => ...之类的代码时,一目了然地更容易理解(再次......因为我对它很熟悉。不久前我感觉完全相反)。

我要说的是:这个讨论归结为:

  1. 当您不习惯它时,如果不损害可读性,它似乎_really_ 奇怪且几乎无用。 有些人对此有或没有一种或另一种感觉。
  2. 你做的函数式编程越多,对这种语法的需求就越明显。 我猜想这与功能概念(如部分应用程序和柯里化)有关,它们为微小的工作引入了许多功能,这些功能对读者来说是噪音。

我们能做的最好的事情就是提供更多的用例。

作为对@dimitropoulos评论的回应,以下是我观点的粗略总结:

我想使用将从该提案中受益匪浅的设计模式(例如函数式编程),因为它们与当前语法的使用过于冗长。

@dimitropoulos我一直在研究 V8,但那是在构建虚拟机,它是用 C++ 编写的。 我对实际 Javascript 的经验是有限的。 也就是说,Javascript 是一种动态类型的语言,没有类型,大部分类型的输入都会消失。 正如一些人之前提出的,这里的一个主要问题是需要重复类型,这是 Javascript 中不存在的问题。

另外,记录一下:在设计 Go 的早期,我们实际上研究了函数签名的箭头语法。 我不记得细节,但我很确定符号,例如

func f (x int) -> float32

在白板上。 最终我们放弃了箭头,因为它不能很好地处理多个(非元组)返回值; 一旦func和参数出现,箭头就多余了; 也许“漂亮”(就像在数学上看起来一样),但仍然是多余的。 它看起来也像是属于一种“不同”语言的语法。

但是在高性能的通用语言中使用闭包打开了通往新的、更具功能性的编程风格的大门。 现在,10 年后,人们可能会从不同的角度看待它。

尽管如此,我认为我们必须非常小心,不要为闭包创建特殊语法。 我们现在拥有的是简单而规律的,并且到目前为止运行良好。 无论采用何种方法,如果有任何变化,我相信它需要是常规的并适用于任何功能。

在 Go 中,函数声明的语法与我们对其他声明的常规模式有所不同。 对于常量、类型、变量,我们总是有:
keyword name type value
[…]
对于这种分解的函数,类型始终必须是文字签名。

请注意,对于参数列表和constvar声明,我们有一个类似的模式IdentifierList Type ,我们可能也应该保留它。 这似乎会排除 lambda-calculus 风格的:标记来将变量名与类型分开。

无论采用何种方法,如果有任何变化,我相信它需要是常规的并适用于任何功能。

keyword name type value模式适用于_declarations_,但@neild提到的用例都适用于_literals_。

如果我们解决文字的问题,那么我相信声明的问题变得微不足道。 对于常量、变量和现在类型的声明,我们允许(或要求)在 value 之前使用value =标记。 将其扩展到函数似乎很容易:

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

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

=标记之后的表达式必须是函数文字,或者可能是由调用返回的函数,其参数在编译时都可用。 在=形式中,仍然可以提供Signature以将参数类型声明从文字移动到FunctionSpec

请注意, 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的提议对我来说是正确的。 它非常接近现有语法,但适用于函数式编程,因为它消除了类型规范的重复。 它并没有_that_ 比(a, b) => a + b紧凑得多,而且它非常适合现有的语法。

@尼尔德

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的建议是迄今为止我见过的最好和最简单的建议。 但是,它不容易允许的一件事是要引用命名结果参数的函数文字的轻量级表示法。 但也许在那种情况下,他们应该使用完整的符号。 (只是有点不规则)。

我们甚至可以将(x, y) { ... }解析为func (x, y T) T { ... }的简写形式; 虽然它需要一些解析器前瞻,但也许还不错。

作为一个实验,我修改了 gofmt 以将函数文字重写为紧凑的语法,并针对 src/ 运行它。 你可以在这里看到结果:

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

我没有试图将其限制在有意义的情况下。 我只是想了解紧凑语法在实践中如何发挥作用。 我还没有深入研究它来对结果发表任何意见。

@neild不错的分析。 一些观察:

  1. 使用:=绑定函数文字的情况令人失望,因为在没有显式类型注释的情况下处理这些情况将需要更复杂的推理算法。

  2. 传递给回调的文字在某些情况下更容易阅读,但在其他情况下更难阅读。
    例如,丢失跨越多行的函数字面量的返回类型信息有点遗憾,因为这也告诉读者他们是在查看函数式 API 还是命令式 API。

  3. 切片内函数字面量的样板代码的减少是可观的。

  4. defergo语句是一个有趣的例子:我们会根据实际传递给函数的参数推断参数类型吗?

  5. 示例中缺少几个尾随的...标记。

defergo确实是一个非常有趣的案例。

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 function literal_ 的 OP 示例的样子:

compute({ $0 + $1 })

我相信这将具有与 Go 1 完全向后兼容的优势。

我刚刚发现这个是因为我正在编写一个简单的 tcp 聊天应用程序,
基本上我有一个里面有一个切片的结构

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

我想同时对其应用一些操作(添加连接,向所有人发送消息等)

而不是遵循复制粘贴互斥锁代码的正常路径,或者使用守护进程goroutine来管理访问,我想我只是通过一个闭包

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

对于短期操作,它过于冗长(仍然比lockdefer unlock()更好)

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

这违反了 DRY 原则,因为我在run方法中输入了确切的函数签名。

如果支持推断函数签名,我可以这样写

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

我不认为这会降低代码的可读性,你可以说它是一个切片,因为append ,而且因为我已经很好地命名了我的变量,你可以猜到它是一个 []net.Conn 而不看在run方法签名处。

我会避免尝试根据函数体推断参数的类型,而是仅在明显的情况下添加推断(例如将闭包传递给函数)。

我想说这不会损害可读性,因为它为读者提供了一个选项,如果他们不知道参数的类型,他们可以godef或将鼠标悬停在它上面并让编辑器向他们展示.

有点像书中他们不重复人物介绍的方式,除了我们会有一个按钮来显示它/跳转到它。

我不擅长写作,所以希望你能幸免于难:)

我认为如果我们将它的使用限制在函数体是一个简单表达式的情况下,这更有说服力。

我敢反对。 这仍然会导致定义函数的两种方式,而我爱上 Go 的原因之一是,虽然它在这里和那里有些冗长,但它具有令人耳目一新的表现力:你知道闭包在哪里,因为有如果您跟踪它,则func关键字或参数是一个函数。

conns.run(func(conns []net.Conn) { conns = append(conns, conn) })
这违反了 DRY 原则,因为我在 run 方法中输入了确切的函数签名。

干燥_是_重要的,毫无疑问。 但是,为了以尽可能少的努力理解代码的能力为代价,将其应用于编程的每个部分,这有点过头了,恕我直言。

我认为这里的一般问题(以及其他一些建议)是讨论主要是关于如何安全地_编写_代码,而恕我直言,它应该是如何安全地_阅读_代码。 多年后有人写了它。 我最近遇到了我的poc.pl ,但我仍在试图弄清楚它的作用……;)

conns.run(func(conns) { conns = append(conns, conn) })
我不认为这会降低代码的可读性,因为 append,你可以说它是一个 slice,并且因为我已经很好地命名了我的变量,你可以猜测它是一个 []net.Conn,而无需查看 run 方法签名.

在我看来,这种说法有几个问题。 我不知道别人怎么看,但我_讨厌_猜测。 一个可能是对的,一个可能是错的,但肯定必须付出努力——为了节省“输入” []net.Conn的好处。 并且代码的可读性和可理解性应该由好的变量名来支持,而不是基于它。

总结:我认为讨论的重点应该从如何减少编写代码时的次要工作转移到如何减少理解所述代码的工作。

我以引用戴夫·切尼(Dave Cheney)引用罗伯特·派克(iirc)的话说结束

清晰胜于聪明。

自动完成可以在一定程度上减轻输入函数签名的乏味。 例如,gopls 提供了创建函数字面量的补全:
cb

我认为这提供了一个很好的中间立场,类型名称仍在源代码中,只剩下一种定义匿名函数的方法,您不必输入整个签名。

会添加还是不添加?
...对于那些不喜欢此功能的人仍然可以使用旧语法。
...对于想要更简单的我们来说,我们可以希望使用这个新功能,我写 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并继续前进。

我不敢相信箭头函数在 Go 语言中仍然不可用。
令人惊讶的是,在 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 我倾向于更长的签名的一个原因是因为它们可以通过类型别名来缩短:

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

将文字缩短为func(gtx C) D { ... }

第二个原因是更长的签名与解决此问题的任何方法都向前兼容。

我带着一个想法来到这里,发现@networkimprov已经在这里提出了类似的建议。

我喜欢使用函数类型(也可以是未命名的函数类型或别名)作为函数字面量的说明符的想法,因为这意味着我们可以对参数和返回值使用通常的类型推断规则,因为我们知道提前准确的类型。 这意味着(例如)自动完成可以像往常一样工作,我们不需要引入时髦的自上而下的类型推断规则。

鉴于:

type F func(a, b int) int

我原来的想法是:

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

但这看起来太像一个普通的函数调用 - 它看起来不像ab在那里定义。

排除其他可能性(我不特别喜欢其中任何一种):

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{...}的语法树。

另一点是语法也不需要在解析器中回溯。 当{是复合文字的一部分还是块的一部分存在歧义时,该歧义总是以有利于后者的方式解决。

我还是很喜欢我在本期早些时候提出的语法,它通过保留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 引人注目。

讨论了使函数文字更简洁的两种主要方法:

  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中看起来很奇怪!)但它可能没问题如果没有歧义,请重用现有的标记,例如“=”。 我将在本文的其余部分使用“=”。

对于类型省略,有两种情况

  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:

我没有看到的一个问题是如何处理...参数。 你可以为任何一个争论

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我坚持认为这只会将文字语法中的混乱推到许多额外的类型定义中。 每个这样的定义至少需要使用两次才能比仅在文字中编写类型更短。

我也不确定它是否会在所有情况下与泛型配合得很好。

考虑相当奇怪的功能

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 文字中类型省略所需的大部分机制。

您也可以为每个调用XT定义一个新类型,但是除非您为每个T调用X多次,否则节省不多.

此页面是否有帮助?
0 / 5 - 0 等级