O que significa a sintaxe “Just” em Haskell?

118

Eu vasculhei a internet em busca de uma explicação real do que essa palavra-chave faz. Cada tutorial Haskell que eu li começa a usá-lo aleatoriamente e nunca explica o que ele faz (e eu olhei muitos).

Aqui está um código básico do Real World Haskell que usa Just. Eu entendo o que o código faz, mas não entendo qual é o propósito ou função de Just.

lend amount balance = let reserve    = 100
                      newBalance = balance - amount
                  in if balance < reserve
                     then Nothing
                     else Just newBalance

Pelo que observei, está relacionado à Maybedigitação, mas é praticamente tudo o que consegui aprender.

Uma boa explicação do que Justsignifica seria muito apreciada.

reem
fonte

Respostas:

211

Na verdade, é apenas um construtor de dados normal que é definido no Prelude , que é a biblioteca padrão que é importada automaticamente para cada módulo.

O que talvez seja, estruturalmente

A definição é mais ou menos assim:

data Maybe a = Just a
             | Nothing

Essa declaração define um tipo,, Maybe aque é parametrizado por uma variável de tipo a, o que significa apenas que você pode usá-lo com qualquer tipo no lugar de a.

Construindo e Destruindo

O tipo possui dois construtores, Just ae Nothing. Quando um tipo tem vários construtores, significa que um valor do tipo deve ter sido construído com apenas um dos construtores possíveis. Para este tipo, um valor foi construído por meio de Justou Nothing, não há outras possibilidades (sem erro).

Como Nothingnão tem tipo de parâmetro, quando é usado como construtor, ele nomeia um valor constante que é membro do tipo Maybe apara todos os tipos a. Mas o Justconstrutor tem um parâmetro de tipo, o que significa que quando usado como um construtor ele atua como uma função de tipo apara Maybe a, ou seja, tem o tipoa -> Maybe a

Portanto, os construtores de um tipo constroem um valor desse tipo; o outro lado das coisas é quando você gostaria de usar esse valor, e é aí que a correspondência de padrões entra em jogo. Ao contrário das funções, os construtores podem ser usados ​​em expressões de vinculação de padrão e esta é a maneira pela qual você pode fazer a análise de caso de valores que pertencem a tipos com mais de um construtor.

Para usar um Maybe avalor em uma correspondência de padrão, você precisa fornecer um padrão para cada construtor, assim:

case maybeVal of
    Nothing   -> "There is nothing!"
    Just val  -> "There is a value, and it is " ++ (show val)

Nesse caso, expressão, o primeiro padrão corresponderia se o valor fosse Nothing, e o segundo corresponderia se o valor fosse construído com Just. Se o segundo corresponder, ele também vincula o nome valao parâmetro que foi passado ao Justconstrutor quando o valor com o qual você está correspondendo foi construído.

O que talvez signifique

Talvez você já saiba como isso funciona; não há realmente nenhuma mágica para os Maybevalores, é apenas um tipo de dados algébrico Haskell (ADT) normal. Mas é bastante usado porque efetivamente "levanta" ou estende um tipo, como Integerno seu exemplo, para um novo contexto no qual ele tem um valor extra ( Nothing) que representa uma falta de valor! O sistema de tipo então requer que você verifique esse valor extra antes de permitir que você obtenha o Integerque pode estar lá. Isso evita um número notável de bugs.

Muitas linguagens hoje lidam com esse tipo de valor "sem valor" por meio de referências NULL. Tony Hoare, um eminente cientista da computação (ele inventou o Quicksort e é um vencedor do Prêmio Turing), reconhece isso como seu "erro de um bilhão de dólares" . O tipo Maybe não é a única maneira de consertar isso, mas provou ser uma maneira eficaz de fazer isso.

Talvez como um Functor

A ideia de transformar um tipo em outro de modo que as operações no tipo antigo também possam ser transformadas para funcionar no novo tipo é o conceito por trás da classe de tipo Haskell chamada Functor, que Maybe atem uma instância útil de.

Functorfornece um método chamado fmap, que mapeia funções que variam de valores do tipo base (como Integer) para funções que variam de valores do tipo levantado (como Maybe Integer). Uma função transformada com fmappara trabalhar em um Maybevalor funciona assim:

case maybeVal of
  Nothing  -> Nothing         -- there is nothing, so just return Nothing
  Just val -> Just (f val)    -- there is a value, so apply the function to it

Portanto, se você tem um Maybe Integervalor m_xe uma Int -> Intfunção f, pode fmap f m_xaplicar a função fdiretamente ao Maybe Integersem se preocupar se ela realmente tem um valor ou não. Na verdade, você poderia aplicar uma cadeia inteira de Integer -> Integerfunções elevadas aos Maybe Integervalores e apenas se preocupar em verificar explicitamente uma Nothingvez quando terminar.

Talvez como uma mônada

Não tenho certeza de como você está familiarizado com o conceito de a Monadainda, mas pelo menos você já usou IO aantes, e a assinatura de tipo IO aé notavelmente semelhante a Maybe a. Embora IOseja especial por não expor seus construtores para você e, portanto, só pode ser "executado" pelo sistema de tempo de execução Haskell, ainda é um Functoralém de ser um Monad. Na verdade, há um sentido importante em que a Monadé apenas um tipo especial de Functorcom alguns recursos extras, mas este não é o lugar para entrar nisso.

De qualquer forma, as Mônadas gostam de IOtipos de mapas para novos tipos que representam "cálculos que resultam em valores" e você pode transformar funções em Monadtipos por meio de uma fmapfunção muito parecida com a chamada liftMque transforma uma função regular em um "cálculo que resulta no valor obtido pela avaliação do função."

Você provavelmente adivinhou (se já leu até aqui) que Maybetambém é a Monad. Ele representa "cálculos que podem não retornar um valor". Assim como no fmapexemplo, isso permite que você faça vários cálculos sem ter que verificar explicitamente os erros após cada etapa. E, de fato, da maneira como a Monadinstância é construída, um cálculo de Maybevalores para assim que um Nothingé encontrado, então é como um aborto imediato ou um retorno sem valor no meio de um cálculo.

Você poderia ter escrito talvez

Como eu disse antes, não há nada inerente ao Maybetipo que está embutido na sintaxe da linguagem ou no sistema de tempo de execução. Se Haskell não o forneceu por padrão, você mesmo poderia fornecer todas as suas funcionalidades! Na verdade, você mesmo poderia escrever novamente, com nomes diferentes, e obter a mesma funcionalidade.

Esperamos que você entenda o Maybetipo e seus construtores agora, mas se ainda houver alguma dúvida, me avise!

Levi Pearson
fonte
19
Que resposta notável! Deve ser mencionado que Haskell freqüentemente usa Maybeonde outras línguas usariam nullou nil(com sórdidos NullPointerExceptions espreitando em cada esquina). Agora, outras linguagens também começam a usar essa construção: Scala as Option, e até mesmo Java 8 terá o Optionaltipo.
Landei de
3
Esta é uma explicação excelente. Muitas das explicações que li sugeriram a ideia de que Just é um construtor do tipo Maybe, mas nenhuma o tornou realmente explícito.
reem
@Landei, obrigado pela sugestão. Fiz uma edição para me referir ao perigo de referências nulas.
Levi Pearson
@Landei O tipo de opção existe desde o ML nos anos 70, é mais provável que o Scala o tenha tirado daí porque o Scala usa a convenção de nomenclatura do ML, Opção com construtores alguns e nenhum.
stonemetal
O Swift da @Landei Apple também utiliza opcionais
Jamin
37

A maioria das respostas atuais são explicações altamente técnicas de como Juste os amigos funcionam; Pensei em tentar explicar para que serve.

Muitas linguagens têm um valor como nullesse que pode ser usado em vez de um valor real, pelo menos para alguns tipos. Isso deixou muitas pessoas muito zangadas e foi amplamente considerado uma má jogada. Ainda assim, às vezes é útil ter um valor como nullindicar a ausência de algo.

Haskell resolve esse problema fazendo com que você marque explicitamente os locais onde pode ter um Nothing(sua versão de a null). Basicamente, se sua função normalmente retornaria o tipo Foo, ela deveria retornar o tipo Maybe Foo. Se quiser indicar que não há valor, volte Nothing. Se quiser retornar um valor bar, você deve retornar Just bar.

Então, basicamente, se você não pode ter Nothing, você não precisa Just. Se você pode ter Nothing, você precisa Just.

Não há nada de mágico nisso Maybe; é construído no sistema de tipo Haskell. Isso significa que você pode usar todos os truques usuais de correspondência de padrões Haskell com ele.

Brent Royal-Gordon
fonte
1
Muito boa resposta ao lado das outras respostas, mas acho que ainda se beneficiaria de um exemplo de código :)
PascalVKooten
13

Dado um tipo t, um valor de Just té um valor existente do tipo t, onde Nothingrepresenta uma falha em alcançar um valor ou um caso em que ter um valor não faria sentido.

No seu exemplo, ter um saldo negativo não faz sentido, então, se isso acontecer, ele é substituído por Nothing.

Para outro exemplo, isso poderia ser usado na divisão, definindo uma função de divisão que recebe ae be retorna Just a/bse bfor diferente de zero e Nothingcaso contrário. Muitas vezes é usado assim, como uma alternativa conveniente para exceções, ou como seu exemplo anterior, para substituir valores que não fazem sentido.

qaphla
fonte
1
Então, digamos que no código acima eu removi o Just, por que isso não funcionaria? Você tem que ter Apenas a qualquer hora que quiser, um Nada? O que acontece com uma expressão em que uma função dentro dessa expressão retorna Nada?
reem
7
Se você removeu o Just, seu código não seria verificado. A razão para isso Justé manter os tipos adequados. Existe um tipo (uma mônada, na verdade, mas é mais fácil pensar que é apenas um tipo) Maybe t, que consiste em elementos da forma Just te Nothing. Uma vez que Nothingtem tipo Maybe t, uma expressão que pode ser avaliada como um Nothingou algum valor de tipo tnão foi digitada corretamente. Se uma função retornar Nothingem alguns casos, qualquer expressão que use essa função deve ter alguma maneira de verificar isso ( isJustou uma instrução case), de modo a tratar todos os casos possíveis.
qaphla de
2
Portanto, o Just está lá simplesmente para ser consistente no tipo Maybe porque um t regular não está no tipo Maybe. Tudo está muito mais claro agora. Obrigado!
reem
3
@qaphla: Seu comentário sobre ser uma "mônada, na verdade, [...]" é enganoso. Maybe t é apenas um tipo. O fato de haver uma Monadinstância para Maybenão o transforma em algo que não seja um tipo.
Sarah
2

Uma função total a-> b pode encontrar um valor do tipo b para cada valor possível do tipo a.

Em Haskell, nem todas as funções são totais. Neste caso particular, a função lendnão é total - não é definida para o caso em que o saldo é menor que a reserva (embora, a meu gosto, faria mais sentido não permitir que newBalance seja menor que a reserva - como está, você pode pegar 101 emprestado de um saldo de 100).

Outros projetos que lidam com funções não totais:

  • lançar exceções ao verificar o valor de entrada não se encaixa no intervalo
  • retornar um valor especial (tipo primitivo): a escolha favorita é um valor negativo para funções inteiras destinadas a retornar números naturais (por exemplo, String.indexOf - quando uma substring não é encontrada, o índice retornado é comumente projetado para ser negativo)
  • retornar um valor especial (ponteiro): NULL ou algo assim
  • retornar silenciosamente sem fazer nada: por exemplo, lendpoderia ser escrito para retornar o saldo antigo, se a condição para o empréstimo não for atendida
  • retornar um valor especial: Nada (ou quebra automática de algum objeto de descrição de erro)

Essas são limitações de projeto necessárias em linguagens que não podem impor a totalidade das funções (por exemplo, Agda pode, mas isso leva a outras complicações, como tornar-se incompleto).

O problema em retornar um valor especial ou lançar exceções é que é fácil para o chamador omitir o tratamento de tal possibilidade por engano.

O problema de descartar silenciosamente uma falha também é óbvio - você está limitando o que o chamador pode fazer com a função. Por exemplo, se o lendsaldo antigo for retornado, o chamador não terá como saber se o saldo mudou. Isso pode ou não ser um problema, dependendo da finalidade pretendida.

A solução de Haskell força o chamador de uma função parcial a lidar com o tipo como Maybe a, ou Either error apor causa do tipo de retorno da função.

Desta forma, lendcomo é definida, é uma função que nem sempre calcula novo saldo - em algumas circunstâncias não é definido novo saldo. Sinalizamos essa circunstância ao chamador retornando o valor especial Nothing ou envolvendo o novo saldo em Just. O chamador agora tem liberdade de escolha: lidar com a falha de empréstimo de uma maneira especial ou ignorar e usar o saldo antigo - por exemplo maybe oldBalance id $ lend amount oldBalance,.

Sassa NF
fonte
-1

A função if (cond :: Bool) then (ifTrue :: a) else (ifFalse :: a)deve ter o mesmo tipo de ifTruee ifFalse.

Então, quando escrevemos then Nothing, devemos usar o Maybe atipoelse f

if balance < reserve
       then (Nothing :: Maybe nb)         -- same type
       else (Just newBalance :: Maybe nb) -- same type
sagacidade
fonte
1
Tenho certeza que você quis dizer algo muito profundo aqui
ver
1
Haskell tem inferência de tipo. Não há necessidade de indicar o tipo Nothinge Just newBalanceexplicitamente.
direito
Para esta explicação para os não iniciados, os tipos explícitos esclarecem o significado do uso de Just.
philip