Go: proposta: Go 2: Sintaxe de função anônima leve

Criado em 17 ago. 2017  ·  53Comentários  ·  Fonte: golang/go

Muitas linguagens fornecem uma sintaxe leve para especificar funções anônimas, nas quais o tipo de função é derivado do contexto circundante.

Considere um exemplo um pouco artificial do tour 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 })

Muitas linguagens permitem elidir o parâmetro e os tipos de retorno da função anônima neste caso, pois podem ser derivados do contexto. Por exemplo:

// 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.

Proponho considerar adicionar tal formulário ao Go 2. Não estou propondo nenhuma sintaxe específica. Em termos de especificação de linguagem, isso pode ser pensado como uma forma de literal de função não tipada que pode ser atribuída a qualquer variável compatível do tipo de função. Literais desta forma não teriam tipo padrão e não poderiam ser usados ​​no lado direito de um := da mesma forma que x := nil é um erro.

Usos 1: Cap'n Proto

Chamadas remotas usando Cap'n Proto recebem um parâmetro de função que recebe uma mensagem de solicitação para preencher. De 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
})

Usando a sintaxe Rust (apenas como exemplo):

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

Usos 2: errgroup

O pacote errgroup (http://godoc.org/golang.org/x/sync/errgroup) gerencia um grupo de goroutines:

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

Usando a sintaxe Scala:

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

(Como a assinatura da função é bem pequena nesse caso, pode ser que a sintaxe leve seja menos clara.)

Go2 LanguageChange Proposal

Comentários muito úteis

Eu apoio a proposta. Economiza digitação e ajuda na legibilidade. Meu caso de uso,

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

sem sintaxe de função anônima leve:

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

com sintaxe de função anônima leve:

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

Todos 53 comentários

Sou solidário com a ideia geral, mas acho que os exemplos específicos dados não são muito convincentes: As economias relativamente pequenas em termos de sintaxe não parecem valer a pena. Mas talvez haja exemplos melhores ou notações mais convincentes.

(Talvez com exceção do exemplo do operador binário, mas não tenho certeza de quão comum esse caso é no código Go típico.)

Por favor, não, claro é melhor do que inteligente. Eu encontro essas sintaxes de atalho
impossivelmente obtuso.

Em sex, 18 de agosto de 2017, 04:43 Robert Griesemer [email protected]
escreveu:

Sou solidário com a ideia geral, mas acho que os exemplos específicos
dado não muito convincente: As economias relativamente pequenas em termos de sintaxe
não parece valer a pena. Mas talvez haja melhores exemplos ou
notação mais convincente.


Você está recebendo isso porque está inscrito neste tópico.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/21498#issuecomment-323159706 ou silenciar
o segmento
https://github.com/notifications/unsubscribe-auth/AAAcAxlgwt-iPryyY-d5w8GJho0bY9bkks5sZInfgaJpZM4O6pBB
.

Acho que isso é mais convincente se restringirmos seu uso aos casos em que o corpo da função é uma expressão simples. Se formos obrigados a escrever um bloco e um return explícito, os benefícios serão um pouco perdidos.

Seus exemplos tornam-se então

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

g.Go(=> nil)

A sintaxe é algo como

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

Isso só pode ser usado em uma atribuição a um valor do tipo de função (incluindo a atribuição a um parâmetro no processo de uma chamada de função). O número de identificadores deve corresponder ao número de parâmetros do tipo de função e o tipo de função determina os tipos de identificador. O tipo de função deve ter zero resultados ou o número de parâmetros de resultado deve corresponder ao número de expressões na lista. O tipo de cada expressão deve ser atribuível ao tipo do parâmetro de resultado correspondente. Isso é equivalente a um literal de função da maneira óbvia.

Provavelmente há uma ambiguidade de análise aqui. Também seria interessante considerar a sintaxe

λ [Identifier] | "(" IdentifierList ")" "." ExpressionList

como em

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

Mais alguns casos em que os fechamentos são comumente usados.

(Estou principalmente tentando coletar casos de uso no momento para fornecer evidências a favor/contra a utilidade desse recurso.)

Eu realmente gosto que Go não discrimine funções anônimas mais longas, como Java faz.

Em Java, uma função anônima curta, um lambda, é agradável e curta, enquanto uma função mais longa é detalhada e feia em comparação com a curta. Eu até vi um talk/post em algum lugar (não consigo encontrá-lo agora) que incentivava apenas o uso de lambdas de uma linha em Java, porque eles têm todas essas vantagens de não-verbosidade.

Em Go, não temos esse problema, tanto as funções anônimas curtas quanto as mais longas são relativamente (mas não muito) detalhadas, então não há nenhum obstáculo mental para usar as mais longas também, o que às vezes é muito útil.

A abreviação é natural em linguagens funcionais porque tudo é uma expressão e o resultado de uma função é a última expressão na definição da função.

Ter uma abreviação é bom, então outras linguagens onde o acima não se aplica a adotaram.

Mas na minha experiência nunca é tão bom quando atinge a realidade de uma linguagem com declarações.

É quase tão detalhado porque você precisa de blocos e retornos ou pode conter apenas expressões, então é basicamente inútil para tudo, exceto para as coisas mais simples.

As funções anônimas em Go são o mais próximo possível do ideal. Eu não vejo o valor em raspar ainda mais.

Não é a sintaxe func que é o problema, são as declarações de tipo redundantes.

Simplesmente permitir que os literais de função eliminem tipos não ambíguos seria um longo caminho. Para usar o exemplo Cap'n'Proto:

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

Sim, são as declarações de tipo que realmente adicionam ruído. Infelizmente, "func (p) error" já tem um significado. Talvez permitir que _ substitua um tipo inferido funcione?

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

Eu gosto disso; nenhuma mudança sintática necessária.

Eu não gosto da gagueira de _. Talvez func possa ser substituído por uma palavra-chave que infere os parâmetros de tipo:
s.Write(ctx, λ(p) { return p.SetData([]byte("Hello, ")) })

Isso é realmente uma proposta ou você está apenas cuspindo como Go ficaria se você o vestisse como Scheme for Halloween? Eu acho que esta proposta é desnecessária e não está de acordo com o foco da linguagem na legibilidade.

Por favor, pare de tentar mudar a sintaxe do idioma só porque ele _parece_ diferente de outros idiomas.

Acho que ter uma sintaxe de função anônima concisa é mais atraente em outras linguagens que dependem mais de APIs baseadas em retorno de chamada. Em Go, não tenho certeza se a nova sintaxe realmente se pagaria. Não é que não existam muitos exemplos em que as pessoas usam funções anônimas, mas pelo menos no código que leio e escrevo a frequência é bastante baixa.

Acho que ter uma sintaxe de função anônima concisa é mais atraente em outras linguagens que dependem mais de APIs baseadas em retorno de chamada.

Até certo ponto, essa é uma condição de auto-reforço: se fosse mais fácil escrever funções concisas em Go, poderíamos ver APIs mais funcionais. (Se isso é bom ou não, eu não sei.)

Quero enfatizar que há uma diferença entre APIs "funcionais" e "callback": quando ouço "callback", penso em "callback assíncrono", o que leva a um tipo de código espaguete que tivemos a sorte de evitar em Vai. APIs síncronas (como filepath.Walk ou strings.TrimFunc ) são provavelmente o caso de uso que devemos ter em mente, pois elas combinam melhor com o estilo síncrono de programas Go em geral.

Eu gostaria apenas de entrar aqui e oferecer um caso de uso em que passei a apreciar a sintaxe lambda do estilo arrow para reduzir bastante o atrito: currying.

considerar:

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

Agora imagine que estamos tentando converter um valor em mongo.FieldConvertFunc ou algo que requer uma abordagem funcional, e você verá que ter uma sintaxe mais leve pode melhorar bastante as coisas ao mudar uma função de não ser curry a ser curry (feliz em fornecer um exemplo mais real, se alguém quiser).

Não convencido? Não pensei assim. Também adoro a simplicidade do go e acho que vale a pena proteger.

Outra situação que acontece muito comigo é onde você tem e quer agora curry o próximo argumento com curry.

agora você teria que mudar
func (a, b) x
para
func (a) func(b) x { return func (b) { return ...... x } }

Se houvesse uma sintaxe de seta você simplesmente mudaria
(a, b) => x
para
(a) => (b) => x

@neild Embora eu ainda não tenha contribuído para este tópico, tenho outro caso de uso que se beneficiaria de algo semelhante ao que você propôs.

Mas este comentário é na verdade sobre outra maneira de lidar com a verbosidade na chamada do código: tenha uma ferramenta como gocode (ou similar) template um valor de função para você.

Tomando seu exemplo:

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

Se assumirmos que digitamos:

var _ = compute(
                ^

com o cursor na posição mostrada pelo ^ ; então invocar tal ferramenta poderia modelar trivialmente um valor de função para você dando:

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

Isso certamente cobriria o caso de uso que eu tinha em mente; cobre o seu?

O código é lido com muito mais frequência do que é escrito. Não acredito que economizar um pouco de digitação valha a pena mudar a sintaxe do idioma aqui. A vantagem, se houver, seria em grande parte tornar o código mais legível. O suporte do editor não ajudará com isso.

Uma questão, é claro, é se a remoção das informações de tipo completo de uma função anônima ajuda ou prejudica a legibilidade.

Não acho que esse tipo de sintaxe reduza a legibilidade, quase todas as linguagens de programação modernas têm uma sintaxe para isso e isso porque incentiva o uso de estilo funcional para reduzir o clichê e tornar o código mais claro e fácil de manter. É muito difícil usar funções anônimas em golang quando elas são passadas como parâmetros para funções porque você tem que se repetir digitando novamente os tipos que você sabe que deve passar.

Eu apoio a proposta. Economiza digitação e ajuda na legibilidade. Meu caso de uso,

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

sem sintaxe de função anônima leve:

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

com sintaxe de função anônima leve:

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

A falta de expressões de função anônimas concisas torna o Go menos legível e viola o princípio DRY. Eu gostaria de escrever e usar APIs funcionais/de retorno de chamada, mas usar essas APIs é irritantemente detalhado, pois cada chamada de API deve usar uma função já definida ou uma expressão de função anônima que repete informações de tipo que devem ser bastante claras do contexto (se a API foi projetada corretamente).

Meu desejo para esta proposta não é nem remotamente que eu acho que Go deve se parecer ou ser como outras linguagens. Meu desejo é inteiramente motivado pela minha aversão por me repetir e incluir ruído sintático desnecessário.

Em Go, a sintaxe para declarações de funções se desvia um pouco do padrão regular que temos para outras declarações. Para constantes, tipos, variáveis ​​sempre temos:

keyword name type value

Por exemplo:

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

Em geral, o tipo pode ser um tipo literal ou pode ser um nome. Para funções que são divididas, o tipo sempre deve ser uma assinatura literal. Pode-se imaginar algo como:

type BinaryOp func(x, y Value) Value

func f BinaryOp { ... }

onde o tipo de função é dado como um nome. Expandindo um pouco, um encerramento BinaryOp poderia então ser escrito como

BinaryOp{ return x.Add(y) }

o que pode percorrer um longo caminho para uma notação de fechamento mais curta. Por exemplo:

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

A principal desvantagem é que os nomes dos parâmetros não são declarados com a função. Usar o tipo de função os traz "no escopo", semelhante a como usar um valor de estrutura x do tipo S traz um campo f para o escopo em uma expressão seletora x.f ou um literal de estrutura S{f: "foo"} .

Além disso, isso requer um tipo de função explicitamente declarado, o que só pode fazer sentido se esse tipo for muito comum.

Apenas outra perspectiva para esta discussão.

A legibilidade vem em primeiro lugar, isso parece ser algo com o qual todos podemos concordar.

Mas dito isso, uma coisa que eu também quero comentar (já que não parece que ninguém disse isso explicitamente) é que a questão da legibilidade sempre dependerá do que você está acostumado. Ter uma discussão como estamos sobre se isso prejudica ou prejudica a legibilidade não vai chegar a lugar nenhum na minha opinião.

@griesemer talvez alguma perspectiva do seu tempo trabalhando no V8 seja útil aqui. Eu (pelo menos) posso dizer que fiquei muito feliz com a sintaxe anterior do javascript para funções ( function(x) { return x; } ) que era (de certa forma) ainda mais pesada de ler do que a de Go agora. Eu estava no campo "essa nova sintaxe é uma perda de tempo" do @douglascrockford .

Mas, mesmo assim, a sintaxe da seta _aconteceu_ e eu aceitei _porque tinha que fazer_. Hoje, porém, tendo usado muito mais e ficado mais confortável com ele, posso dizer que ajuda tremendamente a legibilidade . Eu usei o caso de currying (e @hooluupog trouxe um caso semelhante de "dot-chaining") onde uma sintaxe leve produz um código leve sem ser excessivamente inteligente.

Agora, quando vejo código que faz coisas como x => y => z => ... e é muito mais fácil de entender de relance (de novo... porque estou _familiar_ com ele. não faz muito tempo eu sentia exatamente o oposto).

O que estou dizendo é: essa discussão se resume a:

  1. Quando você não está acostumado com isso, parece _realmente_ estranho e quase inútil, se não prejudicial à legibilidade. Algumas pessoas simplesmente têm ou não um sentimento de uma forma ou de outra sobre isso.
  2. Quanto mais programação funcional você estiver fazendo, mais a necessidade de tal sintaxe se pronuncia. Eu acho que isso tem algo a ver com conceitos funcionais (como aplicação parcial e currying) que introduzem muitas funções para pequenos trabalhos que se traduzem em ruído para o leitor.

A melhor coisa que podemos fazer é fornecer mais casos de uso.

Em resposta ao comentário de @dimitrópoulos , aqui está um resumo da minha visão:

Eu quero usar padrões de design (como programação funcional) que se beneficiariam muito com essa proposta, pois seu uso com a sintaxe atual é excessivamente detalhado.

@dimitrópoulos Eu tenho trabalhado bem no V8, mas isso foi construir a máquina virtual, que foi escrita em C++. Minha experiência com Javascript real é limitada. Dito isso, Javascript é uma linguagem tipada dinamicamente e, sem tipos, grande parte da digitação desaparece. Como várias pessoas mencionaram antes, um grande problema aqui é a necessidade de repetir tipos, um problema que não existe em Javascript.

Além disso, para o registro: nos primeiros dias do projeto Go nós realmente olhamos para a sintaxe de seta para assinaturas de função. Não me lembro dos detalhes, mas tenho certeza de que notações como

func f (x int) -> float32

estava no quadro branco. Eventualmente, nós descartamos a seta porque ela não funcionava muito bem com valores de retorno múltiplos (não-tupla); e uma vez que o func e os parâmetros estivessem presentes, a seta era supérflua; talvez "bonito" (como na aparência matemática), mas ainda supérfluo. Também parecia uma sintaxe que pertencia a um tipo "diferente" de linguagem.

Mas ter closures em uma linguagem performática e de propósito geral abriu as portas para novos estilos de programação mais funcionais. Agora, daqui a 10 anos, pode-se olhar para isso de um ângulo diferente.

Ainda assim, acho que temos que ter muito cuidado aqui para não criar sintaxe especial para encerramentos. O que temos agora é simples e regular e funcionou bem até agora. Seja qual for a abordagem, se houver alguma mudança, acredito que ela precisará ser regular e se aplicar a qualquer função.

Em Go, a sintaxe para declarações de funções se desvia um pouco do padrão regular que temos para outras declarações. Para constantes, tipos, variáveis ​​sempre temos:
keyword name type value
[…]
Para funções que são divididas, o tipo sempre deve ser uma assinatura literal.

Observe que para listas de parâmetros e declarações const e var temos um padrão semelhante, IdentifierList Type , que provavelmente também devemos preservar. Parece que isso descartaria o token : estilo lambda-calculus para separar nomes de variáveis ​​de tipos.

Seja qual for a abordagem, se houver alguma mudança, acredito que ela precisará ser regular e se aplicar a qualquer função.

O padrão keyword name type value é para _declarations_, mas os casos de uso que @neild menciona são todos para _literals_.

Se abordarmos o problema dos literais, acredito que o problema das declarações se torne trivial. Para declarações de constantes, variáveis ​​e agora tipos, permitimos (ou exigimos) um token = antes do value . Parece que seria fácil estender isso para funções:

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

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

A expressão após o token = deve ser uma função literal, ou talvez uma função retornada por uma chamada cujos argumentos estejam todos disponíveis em tempo de compilação. No formulário = , um Signature ainda pode ser fornecido para mover as declarações de tipo de argumento do literal para o FunctionSpec .

Observe que a diferença entre um ShortParameterDecl e o ParameterDecl existente é que IdentifierList s singleton são interpretados como nomes de parâmetros em vez de tipos.


Exemplos

Considere esta declaração de função aceita hoje:

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

Poderíamos manter isso (por exemplo, para compatibilidade com Go 1) além dos exemplos abaixo, ou eliminar a produção Function e usar apenas a versão ShortFunctionLit .

Para várias opções ShortFunctionLit , a gramática que proponho acima fornece:

Tipo ferrugem:

ShortFunctionLit = "|" ShortParameterList "|" Block .

Admite qualquer um:

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

Pessoalmente, acho que todas, exceto as variantes do tipo Scala, são bastante legíveis. (A meu ver, a variante do tipo Scala é muito pesada em parênteses: torna as linhas muito mais difíceis de digitalizar.)

Pessoalmente, estou interessado principalmente nisso se me permitir omitir os tipos de parâmetro e resultado quando eles podem ser inferidos. Estou até bem com a sintaxe literal da função atual, se puder fazer isso. (Isso foi discutido acima.)

É certo que isso vai contra o comentário de @griesemer .

Seja qual for a abordagem, se houver alguma mudança, acredito que ela precisará ser regular e se aplicar a qualquer função.

Eu não acompanho isso. As declarações de função necessariamente devem incluir as informações completas do tipo da função, pois não há como derivá-la com precisão suficiente do corpo da função. (Este não é o caso para todos os idiomas, é claro, mas é para Go.)

Literais de função, por outro lado, podem inferir informações de tipo a partir do contexto.

@neild Desculpas por ser impreciso: O que eu quis dizer com essa frase é que, se houvesse uma nova sintaxe diferente (setas ou o que você tem), ela deveria ser um pouco regular e aplicada em todos os lugares. Se for possível que os tipos possam ser omitidos, isso seria novamente ortogonal.

@griesemer Obrigado; Eu (principalmente) concordo com esse ponto.

Acho que a questão interessante para esta proposta é se ter alguma sintaxe é uma boa ideia ou não; o que essa sintaxe seria é importante, mas relativamente trivial.

No entanto, não consigo resistir à tentação de rejeitar um pouco minha própria proposta.

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

A proposta de @neild parece certa para mim. É bem próximo da sintaxe existente, mas funciona para programação funcional, pois elimina a repetição das especificações de tipo. Não é _isso_ muito menos compacto que (a, b) => a + b , e se encaixa bem na sintaxe existente.

@neild

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

Isso declararia uma variável ou uma função? Se uma variável, como seria a declaração da função equivalente?

No meu esquema de declaração acima, se estou entendendo corretamente, seria:

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

Eu não acho que sou um fã: ele gagueja um pouco em func , e não parece fornecer uma quebra visual suficiente entre o token func e os parâmetros que se seguem.

Ou você deixaria de fora os parênteses da declaração, em vez de atribuir a literais?

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

Eu ainda não gosto da falta de quebra visual, embora...

Isso declararia uma variável ou uma função? Se uma variável, como seria a declaração da função equivalente?

Uma variável. A declaração de função equivalente presumivelmente seria func sum a, b { return a+b } , mas isso seria inválido por razões óbvias - você não pode eliminar tipos de parâmetro de declarações de função.

A mudança gramatical que estou pensando seria algo como:

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

Um literal de função curto é diferenciado de um literal de função regular omitindo os parênteses na lista de parâmetros, define apenas os nomes dos parâmetros de entrada e não define os parâmetros de saída. Os tipos de parâmetros de entrada e os tipos e número de parâmetros de saída são derivados do contexto circundante.

Não acho que haja necessidade de permitir a especificação de tipos de parâmetros opcionais em um literal de função curto; você apenas usa um literal de função regular nesse caso.

Como @ianlancetaylor apontou, a notação leve realmente só faz sentido quando permite a omissão de tipos de parâmetros porque eles podem ser inferidos facilmente. Sendo assim, a sugestão de @neild é a melhor e mais simples que já vi. A única coisa que não permite facilmente é uma notação leve para literais de função que desejam se referir a parâmetros de resultado nomeados. Mas talvez nesse caso eles devam usar a notação completa. (É apenas um pouco irregular).

Podemos até ser capazes de analisar (x, y) { ... } como forma abreviada para func (x, y T) T { ... } ; embora exija um pouco de antecipação do analisador, mas talvez não seja tão ruim.

Como um experimento, modifiquei gofmt para reescrever literais de função na sintaxe compacta e executei-o em src/. Você pode ver os resultados aqui:

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

Não fiz nenhuma tentativa de limitar isso aos casos em que faz sentido; Eu só queria ter uma noção de como a sintaxe compacta pode funcionar na prática. Ainda não pesquisei o suficiente para desenvolver qualquer opinião sobre os resultados.

@neild Bela análise. Algumas observações:

  1. A fração de casos em que o literal da função é vinculado usando := é decepcionante, pois lidar com esses casos sem anotações de tipo explícitas exigiria um algoritmo de inferência mais complicado.

  2. Os literais passados ​​para retornos de chamada são mais fáceis de ler em alguns casos, mas mais difíceis em outros.
    Por exemplo, perder as informações de tipo de retorno para literais de função que abrangem muitas linhas é um pouco infeliz, pois isso também informa ao leitor se ele está olhando para uma API funcional ou imperativa.

  3. A redução no clichê para literais de função dentro de fatias é substancial.

  4. As instruções defer e go são um caso interessante: inferiríamos os tipos de argumento dos argumentos realmente passados ​​para a função?

  5. Alguns tokens ... à direita estão faltando nos exemplos.

defer e go são de fato um caso bastante interessante.

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

Derivaríamos o tipo de p do parâmetro da função real? Isso seria muito bom para instruções go , embora você possa obter o mesmo efeito usando apenas um encerramento:

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

Eu apoiaria totalmente isso. Francamente, não me importo com o quanto "se parece com outras linguagens", só quero uma maneira menos detalhada de usar funções anônimas.

EDIT: Emprestando a sintaxe literal composta ...

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?

Apenas uma ideia:
Aqui está como o exemplo do OP ficaria com uma _função não tipada literal_ com a sintaxe do Swift:

compute({ $0 + $1 })

Acredito que isso teria a vantagem de ser totalmente compatível com o Go 1.

Acabei de encontrar isso porque estava escrevendo um aplicativo simples de bate-papo tcp,
basicamente eu tenho uma estrutura com uma fatia dentro dela

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

e gostaria de aplicar algumas operações a ele simultaneamente (adicionando conexões, enviando mensagens para todos etc.)

e em vez de seguir o caminho normal de copiar e colar o código de bloqueio mutex, ou usar um daemon goroutine para gerenciar o acesso, pensei em passar um encerramento

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

para operações curtas, é excessivamente detalhado (ainda melhor que lock e defer unlock() )

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

Isso viola o princípio DRY, pois digitei essa assinatura de função exata no método run .

Se for suportado inferindo a assinatura da função, eu poderia escrevê-la assim

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

Eu não acho que isso torna o código menos legível, você pode dizer que é uma fatia por causa de append , e porque eu nomeei minhas variáveis ​​bem, você pode adivinhar que é um []net.Conn sem olhar na assinatura do método run .

Eu evitaria tentar inferir os tipos de parâmetros com base no corpo da função, em vez disso, adicionaria inferência apenas para casos em que é óbvio (como passar encerramentos para funções).

eu diria que isso não prejudica a legibilidade, pois dá ao leitor uma opção, se eles não souberem o tipo do parâmetro, podem godef ou passar o mouse sobre ele e fazer com que o editor mostre a eles .

Mais ou menos como em um livro eles não repetem a introdução dos personagens, exceto que teríamos um botão para mostrar / pular para ele.

Eu sou ruim em escrever, então espero que você tenha sobrevivido lendo isso :)

Acho que isso é mais convincente se restringirmos seu uso aos casos em que o corpo da função é uma expressão simples.

Atrevo-me a contestar. Isso ainda levaria a duas maneiras de definir uma função, e uma das razões pelas quais me apaixonei por Go é que, embora tenha alguma verbosidade aqui e ali, tem uma expressividade refrescante: você vê onde está um encerramento porque há ou uma palavra-chave func ou o parâmetro é uma função, se você rastreá-lo.

conns.run(func(conns []net.Conn) { conns = append(conns, conn) })
Isso viola o princípio DRY, pois digitei essa assinatura de função exata no método run.

DRY _é_ importante, sem dúvida. Mas aplicá-lo a cada parte da programação para manter o princípio ao custo da capacidade de entender o código com o menor esforço possível, é um pouco além da marca, imho.

Eu acho que o problema geral aqui (e algumas outras propostas) é que a discussão é principalmente sobre como fazer um esforço seguro _escrevendo_ o código, enquanto que deveria ser como fazer um esforço seguro _ler_ o código. Anos depois de ter sido escrito. Recentemente encontrei um poc.pl meu e ainda estou tentando descobrir o que ele faz... ;)

conns.run(func(conns) { conns = append(conns, conn) })
Eu não acho que isso torna o código menos legível, você pode dizer que é uma fatia por causa do append, e porque eu nomeei minhas variáveis ​​bem, você pode adivinhar que é um []net.Conn sem olhar para a assinatura do método run .

Do meu ponto de vista, há vários problemas com esta afirmação. Eu não sei como os outros veem isso, mas eu _detesto_ adivinhar. Um pode estar certo, outro pode estar errado, mas certamente é preciso se esforçar para isso - para o benefício de economizar para „digitar“ []net.Conn . E a legibilidade e a compreensão do código devem ser suportadas por bons nomes de variáveis, não baseados neles.

Para concluir: acho que o foco da discussão deve mudar de como reduzir pequenos esforços ao escrever código para como reduzir esforços para compreender esse código.

Termino citando Dave Cheney citando Robert Pike (iirc)

Claro é melhor do que inteligente.

O tédio de digitar assinaturas de função pode ser um pouco aliviado pela conclusão automática. Por exemplo, gopls oferece completações que criam literais de função:
cb

Acho que isso fornece um bom meio-termo onde os nomes dos tipos ainda estão no código-fonte, resta apenas uma maneira de definir uma função anônima e você não precisa digitar a assinatura inteira.

será adicionado ou não?
... para quem não gosta desse recurso ainda pode usar a sintaxe antiga.
... para nós que queremos mais simplicidade, podemos usar esse novo recurso espero, já faz 1 ano desde que escrevi go, não tenho certeza se a comunidade ainda acha que isso é importante,
... isso será adicionado ou não?

@noypi Nenhuma decisão foi tomada. Esta questão permanece em aberto.

https://golang.org/wiki/NoPlusOne

Eu apoio essa proposta e acho que esse recurso, em conjunto com os genéricos, tornaria a programação funcional em Go mais amigável ao desenvolvedor.

Aqui está o que eu gostaria de ver, aproximadamente:

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

Eu adoraria poder digitar (a, b) => a * b e seguir em frente.

Eu não posso acreditar que as funções de seta ainda não estão disponíveis em Go lang.
É incrível como é claro e simples trabalhar com Javascript.

JavaScript pode implementar isso de forma trivial, pois não se importa com os parâmetros, o número deles, os valores ou seus tipos até que eles sejam realmente usados.

Poder omitir tipos em literais de função ajudaria muito com o estilo funcional que uso para a API de layout Gio. Veja os muitos literais "func() {...}" em https://git.sr.ht/~eliasnaur/gio/tree/master/example/kitchen/kitchen.go? Sua assinatura real deveria ter sido algo como

func(gtx layout.Context) layout.Dimensions

mas por causa dos nomes de tipo longos, o gtx é um ponteiro para um layout.Context compartilhado que contém os valores de entrada e saída de cada chamada de função.

Provavelmente vou mudar para as assinaturas mais longas, independentemente desse problema, para maior clareza e correção. No entanto, acredito que meu caso é um bom relato de experiência em suporte a literais de função mais curtos.

PS Uma razão pela qual estou me inclinando para as assinaturas mais longas é porque elas podem ser encurtadas por aliases de tipo:

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

que encurta os literais para func(gtx C) D { ... } .

Uma segunda razão é que as assinaturas mais longas são compatíveis com o que quer que resolva esse problema.

Vim aqui com uma ideia e descobri que a @networkimprov já havia sugerido algo parecido aqui .

Gosto da ideia de usar um tipo de função (também pode ser um tipo de função sem nome ou alias) como especificador para um literal de função, porque significa que podemos usar as regras usuais de inferência de tipos para parâmetros e valores de retorno, porque sabemos o tipos exatos com antecedência. Isso significa que (por exemplo) o preenchimento automático pode funcionar como de costume e não precisaríamos introduzir regras de inferência de tipo top-down descoladas.

Dado:

type F func(a, b int) int

meu pensamento inicial era:

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

mas isso se parece muito com uma chamada de função normal - não parece que a e b estão sendo definidos lá.

Jogando fora outras possibilidades (não gosto de nenhuma delas particularmente):

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}

Talvez haja alguma sintaxe legal à espreita aqui em algum lugar :)

Um ponto-chave da sintaxe literal composta é que ela não requer informações de tipo no analisador. A sintaxe para structs, arrays, slices e maps é idêntica; o analisador não precisa saber o tipo de T para gerar uma árvore de sintaxe para T{...} .

Outro ponto é que a sintaxe também não requer retrocesso no analisador. Quando há ambiguidade se um { faz parte de um literal composto ou de um bloco, essa ambiguidade é sempre resolvida em favor do último.

Eu ainda gosto da sintaxe que propus em algum lugar anteriormente nesta edição, que evita qualquer ambiguidade do analisador, mantendo a palavra-chave func :

func a, b { return a + b }

Eu removi meu :-1:. Ainda não estou :+1: nisso, mas estou reconsiderando minha posição. Os genéricos vão causar um aumento nas funções curtas como genericSorter(slice, func(a, b T) bool { return a > b }) . Também achei https://github.com/golang/go/issues/37739#issuecomment -624338848 atraente.

Há duas maneiras principais sendo discutidas para tornar os literais de função mais concisos:

  1. uma forma abreviada para corpos que retornam uma expressão
  2. eliminando os tipos em literais de função.

Acho que ambos devem ser tratados separadamente.

Se FunctionBody for alterado para algo como

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

isso ajudaria principalmente literais de função com ou sem elisão de tipo e também permitiria que declarações de função e método muito simples fossem mais leves na página:

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 e amigos ainda podiam esconder o corpo)

Eu usei a sintaxe de @ianlancetaylor nesse exemplo, a principal desvantagem é que requer a introdução de um novo token (e um que pareceria estranho em func(c chan T) -> <-c !), mas pode ser bom reutilize um token existente, como "=", se não houver ambiguidade. Usarei "=" no restante deste post.

Para o tipo elisão existem dois casos

  1. algo que sempre funciona
  2. algo que só funciona em um contexto onde os tipos podem ser deduzidos

Usar tipos nomeados como @griesemer sugerido sempre funcionaria. Parece haver alguns problemas com a sintaxe. Tenho certeza que isso poderia ser trabalhado. Mesmo que fossem, não tenho certeza se resolveria o problema. Isso exigiria uma proliferação de tipos nomeados. Estes estariam no pacote definindo o local onde são usados ​​ou teriam que ser definidos em cada pacote que os utiliza.

No primeiro você obtém algo como

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

e no último você obtém algo como

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

De qualquer forma, há confusão suficiente para que realmente não reduza muito o clichê, a menos que cada nome seja muito usado.

Uma sintaxe como a de @neild só poderia ser usada quando os tipos pudessem ser deduzidos. Um método simples seria como no #12854, apenas listar todos os contextos em que o tipo é conhecido – parâmetro para uma função, sendo atribuído a um campo, enviado em um canal e assim por diante. O caso go/defer que @neild trouxe parece útil para incluir também.

Essa abordagem especificamente não permite o seguinte

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

mas esses são casos em que valeria a pena ser mais explícito, mesmo que fosse possível inferir o tipo algoritmicamente examinando onde e como eles são usados.

Ele permite muitos casos úteis, incluindo os mais úteis/solicitados:

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

poder escolher usar um bloco independente da sintaxe literal também permite:

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

que é um caso particular cada vez mais me empurrando para um :+1:

Uma questão que não vi levantada é como lidar com parâmetros ... . Você poderia fazer um argumento para qualquer um

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

Eu não tenho uma resposta para isso.

@jimmyfrasche

  1. eliminando os tipos em literais de função.

Eu acredito que isso deve ser tratado com a adição de literais do tipo função. Onde o tipo substitui 'func' e os tipos de argumento são emitidos (como são definidos pelo tipo). Isso mantém a legibilidade e é bastante consistente com os literais para outros tipos.

http.Handle("/", http.HandlerFunc[w, r]{
    fmt.Fprinf(w, "Hello World")
})
  1. uma forma abreviada para corpos que retornam uma expressão

Refatore a função como seu próprio tipo e então as coisas ficam muito mais limpas.

type ComputeFunc func(float64, float64) float64

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

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

Se isso for muito detalhado para você, digite alias o tipo de função dentro do seu código.

{
    type f = ComputeFunc

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

No caso especial de uma função sem argumentos, os colchetes devem ser omitidos.

type IntReturner func() int

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

Eu escolho colchetes porque a proposta de contratos já está usando colchetes padrão extras para funções genéricas.

@Splizard Defendo o argumento de que isso apenas empurraria a desordem da sintaxe literal para muitas definições de tipo extras. Cada uma dessas definições precisaria ser usada pelo menos duas vezes antes de poder ser mais curta do que apenas escrever os tipos no literal.

Também não tenho certeza se funcionaria muito bem com genéricos em todos os casos.

Considere a função bastante estranha

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

Você pode nomear um tipo genérico a ser usado com X :

type XFunc(type T) func() T

Se apenas a definição de XFunc for usada para derivar os tipos dos parâmetros, ao chamar X você precisará informar qual T usar, mesmo que isso seja determinado por o tipo de v :

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

Pode haver um caso especial para cenários como este para permitir que T seja inferido, mas então você acabaria com grande parte do maquinário que seria necessário para elisão de tipo em literais func.

Você também pode definir um novo tipo para cada T com o qual você chama X , mas não há muita economia, a menos que você chame X muitas vezes para cada T .

Esta página foi útil?
0 / 5 - 0 avaliações