Languages: PT, EN
É uma pequena biblioteca em Go que contém a implementação de mônadas (monads) comuns em programação funcional que faz uso do suporte a generics.
go get github.com/MisterKaiou/go-functional
A maneira atual de se retornar um valor ou um erro, com uma tupla ([valor], error)
, não é muito expansível. Visto que você teria de recorrer ao clássico if err != nil { do() }
para tratar o erro, o que fica rapidamente cansativo.
É claro que em linguagens funcionais isso dificilmente seria um problema já que toda a linguagem e suas APIs são criadas com mônadas em mente. Não temos isso nas APIs em Go, mas mesmo que faça somente no seu código, é uma drástica mudança que vai requerer, no máximo, se acostumar a ler result.Map(** parâmetros **)
, o que na minha opinião, não é muito a se pedir levando em consideração quão enxuto o código fica sem as inúmeras linhas de if err != nil
.
- O valor de Ok para um
Result
de erro énil
; - O valor de Ok para um
Result
de sucesso é o valor que ele guarda; - O valor de erro para um
Result
de erro é o erro que foi usado para criá-lo; - O valor de erro para um
Result
de sucesso énil
.
- O valor de Some para um
Option
que representa nada énil
; - O valor de Some para um
Option
que representa algo é o valor que ele guarda; - O valor de None para um
Option
que representa nada é um ponteiro para um instância recêm criada deNone
- O valor de None para um
Option
que representa algo énil
Voce não tem acesso aos valores internos, e isso é por design, você realmente não deveria fazer isso ou o propósito destes tipos seria em vão. Unwrap
é uma conveniência e ainda assim desencorajo o uso dela.
Visando não prolongar demais a descrição, os exemplos de como manusear casos de erro ou None serão mostrados apenas uma vez. Já que, o mesmo comportamento acontece em todos os tipos e funções.
Result[Of any] é uma struct que representa um resultado que contém Of
ou error
. Já que temos uma interface para representar erros dentro da própria linguagem, optei por fazer uso dela.
String: Result
implementa a interface stringer
, e o valor retornado ao chamar o método String
depende do seu valor interno.
- Caso seja Ok:
String
vai retornarfmt.String
aplicado ao valor interno. Ex:
str := result.Ok(42).String() // É o mesmo que fmt.String(42)
print(str) // Imprime: "42"
- Caso seja Error: String vai retornar
error.Error()
no valor interno. Ex:
str := result.Error[int](errors.New("erro")).String() // É o mesmo que err.Error()
print(str) // Imprime: erro
Map: Dado um Result
, ela aceita uma função que recebe o valor deste e retorna outro valor. Por fim, o valor retornado é guardado dentro de um novo Result
Ok:
res := result.Ok(42) // *Result[int]
mappedRes := result.Map(res, func(val int) bool { return val == 42 }) // *Result[bool]
printf("O valor de mappedRes é: %s", mappedRes)
// Imprime na tela: O valor de mappedRes é: true"
Erro:
res := result.Error[int](errors.New("erro")) // *Result[int]
mappedRes := result.Map(res, func(val int) bool { return val == 0 }) //*Result[bool]
printf("O valor de mappedRes é: %s", mappedRes)
// Imprime na tela: O valor de mappedRes é: erro"
MapError: Dado um Result
, ela aceita uma função que recebe o erro deste e retorna outro erro. Por fim, o valor retornado é guardado em um novo Result
.
res := Error[int](errors.New("something failed"))
mapped := MapError(res, func(err error) error { return errors.New(fmt.Sprint("oh no ", err)) })
println(mapped.String()) // Imprime: oh no something failed
Bind: Dado um Result
, ela aceita uma função que recebe o valor deste e retorna outro Result
.
res := result.Ok(42) // *Result[int]
bRes := result.Bind(res, func(val int) bool { return result.Ok(val == 42) }) // *Result[bool]
printf("O valor de bRes é: %s", bRes)
// Imprime na tela: O valor de bRes é: true"
Match: Dado um Result
, ela aceita duas funções que retornam um valor do mesmo tipo, mas a primeira recebe o resultado contido nele e a segunda recebe o erro.
res := result.Ok(42)
matchedRes := result.Match(res,
func(ok int) string { return "Tudo okay por aqui" },
func(err error) string { return err.Error() })
printf("O valor de matchedRes é: %s", matchedRes)
// Imprime na tela: O valor de matchedRes é: Tudo okay por aqui"
Unwrap: É um método de Result
. Retorna o valor contido nele ou chama panic
.
Obs: Prefira usar Match
ao invés de Unwrap
res := result.Ok(69).Unwrap() // res = 69
// Ou
res := result.Error[bool](errors.New("erro")).Unwrap() // Panic
Option[Of any] é uma struct que representa ou algo ou nada.
String: Option
implementa a interface stringer
, e o valor ao chamar o método String
depende do seu valor interno.
- Caso seja Some:
String
vai retornar o valor defmt.String
aplicado ao valor interno. Ex:
str := option.Some(42).String() // É o mesmo que fmt.String(42)
print(str) // Imprime: "42"
- Caso seja None: String vai retornar
"None"
. Ex:
str := option.None[int]().String()
print(str) // Imprime: None
Map: Dado um Option
, ela aceita uma função que recebe o valor deste e retorna outro valor. Por fim, o valor retornado é guardado dentro de um novo Option
Ok:
opt := option.Some(42) // *Option[int]
mappedOpt := option.Map(opt, func(val int) bool { return val == 42 }) // *Option[bool]
printf("O valor de mappedOpt é: %s", mappedOpt)
// Imprime na tela: O valor de mappedOpt é: true"
Erro:
opt := option.None[int]() // *Option[int]
mappedOpt := option.Map(opt, func(val int) bool { return val == 0 }) //*Option[bool]
printf("O valor de mappedOpt é: %s", mappedOpt)
// Imprime na tela: O valor de mappedOpt é: None"
Bind: Dado um Option
, ela aceita uma função que recebe o valor deste e retorna outro Option
.
opt := option.Some(42) // *Option[int]
bRes := option.Bind(opt, func(val int) bool { return option.Some(val == 42) }) // *Option[bool]
printf("O valor de bRes é: %s", bRes)
// Imprime na tela: O valor de bRes é: true"
Match: Dado um Option
, aceita duas funções que retornam um valor do mesmo tipo. A primeira recebe o resultado contido nele e a segunda não recebe parâmetros.
opt := option.Some(42)
matchedOpt := option.Match(opt,
func(ok int) string { return "Algo aqui" },
func() string { return "Nada aqui" })
printf("O valor de matchedOpt é: %s", matchedOpt)
// Imprime na tela: O valor de matchedOpt é: Algo aqui"
Unwrap: É um método de Option
. Retorna o valor contido nele ou chama panic
.
Obs: Prefira usar Match
ao invés de Unwrap
res := option.Some(69).Unwrap() // res = 69
// Ou
res := option.None[bool]().Unwrap() // Panic
O trecho de código abaixo foi retirado de uma aplicação real minha utilizando esta biblioteca em conjunto com gin:
func (h *Handler) Login() response.HttpResult {
nonce := h.NonceGenerator()
stateStr := r.Bind(nonce, func(it auth.Nonce) *r.Result[string] {
saved := h.NonceStore.Save(it)
return r.Map(saved, func(_ unit.Unit) string {
return it.String()
})
})
authUrl := r.Bind(stateStr, auth.GenerateAuthenticationURL)
return response.Redirect(authUrl)
}
Fora as abstrações que eu criei, como response.HttpResult
e response.Redirect
, perceba que cada chamada a Bind
ou Map
seria if err != nil { return/panic }
. Um método que somente cria a URL de login OAuth teria bem mais do que estas 9 linhas caso não estivéssemos usando este estilo de código.
Adicionar o alias r
para o import
ajuda a tornar a leitura menos cansativa.
Adicionar outras mônadas como:
- IO
- Either
- State (A necessidade desta em Go é discutível já que não temos imutabilidade, a menos que possamos considerar Passar por Valor algo próximo a isso)
Os casos de uso para estas são meio raros, por isso não está incluso na biblioteca como está agora. Mas, caso seja um requisito para pelo menos alguns, será implementada.