1 pontos por GN⁺ 2 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • A Mercury oferece serviços bancários para mais de 300 mil empresas com uma base de código de cerca de 2 milhões de linhas de Haskell, excluindo comentários e afins, e em 2025 processou US$ 248 bilhões em volume transacionado e US$ 650 milhões em receita anualizada
  • O valor do uso de Haskell na Mercury está menos na pureza em si e mais em colocar o conhecimento operacional em APIs e tipos, manter comportamentos perigosos atrás de fronteiras estreitas e fazer do caminho seguro o caminho mais fácil
  • A confiabilidade não é tratada como a prevenção de toda falha, mas como a capacidade do sistema de absorver variabilidade, e o sistema de tipos exclui classes de erros e preserva o conhecimento institucional como uma forma de documentação que o compilador faz cumprir
  • A Mercury usa o Temporal como framework de durable execution para retries, timeouts, cancelamentos e recuperação de falhas em fluxos de trabalho financeiros, e publicou em open source o SDK de Haskell hs-temporal-sdk
  • O valor de Haskell em produção não está em colocar tudo nos tipos, mas em proteger com tipos os invariantes que podem levar a perda de dados, erros financeiros e problemas regulatórios, enquanto a complexidade é encapsulada e operada junto com testes, documentação e code review

A escala operacional de Haskell na Mercury e sua visão de confiabilidade

  • A Mercury opera uma base de código Haskell de cerca de 2 milhões de linhas, excluindo comentários e afins
  • A Mercury é uma fintech que oferece serviços bancários para mais de 300 mil empresas e, em 2025, processou US$ 248 bilhões em volume transacionado e US$ 650 milhões em receita anualizada
  • A empresa tem cerca de 1.500 funcionários, e a organização de engenharia contrata principalmente desenvolvedores generalistas; a maioria nunca havia usado Haskell antes de entrar
  • Esse sistema vem operando há anos sob crescimento acelerado, o cenário em que US$ 2 bilhões em novos depósitos entraram em 5 dias durante a crise do SVB, escrutínio regulatório e situações comuns e incomuns de sistemas financeiros em larga escala

Confiabilidade não é prevenir falhas, mas absorver variabilidade

  • A abordagem tradicional de confiabilidade se concentra em enumerar falhas, adicionar verificações e testes e encontrar bugs, mas isso sozinho não basta
  • A Mercury trata confiabilidade como a capacidade do sistema de absorver variabilidade
    • O sistema deve ser capaz de degradar de forma elegante
    • Os operadores devem conseguir entender e ajustar o sistema
    • A arquitetura deve tornar fácil fazer a coisa certa e difícil fazer a coisa errada
  • Em uma organização que cresce rápido, perguntas operacionais reais passam a ser se um novo engenheiro consegue ler e entender um módulo, se um serviço cai junto quando o banco de dados fica lento e se o compilador captura o uso incorreto de interfaces
  • O sistema de tipos está mais próximo de um mecanismo de apoio operacional do que de uma simples prova de correção
    • Exclui determinadas classes de erro
    • Preserva o conhecimento institucional em uma forma que o compilador consegue ler, mesmo depois que o autor saiu
    • Funciona como documentação imposta de forma mais consistente do que uma wiki
  • A engenharia de estabilidade na Mercury não é uma polícia da qualidade que atrasa o desenvolvimento de produto, mas uma forma colaborativa de lidar com o impacto de uma funcionalidade quebrada desde o início do design
    • Raio de explosão em caso de falha
    • Quais operações precisam de idempotência e de que forma
    • Como será o rollback
    • Como lidar com trabalhos em andamento
    • Avaliar antecipadamente quais sistemas absorvem falhas e quais as amplificam

Pureza não é uma propriedade da linguagem, mas uma fronteira de interface

  • A pureza em Haskell não significa que internamente não haja nenhum efeito colateral, mas sim que a interface cria uma fronteira que impede o vazamento de efeitos colaterais
  • Por trás de funções puras em bibliotecas como bytestring, text e vector existem implementações internas com alocação mutável, escrita em buffer e unsafe coercion
  • A mônada ST usa mutações in-place observáveis e efeitos colaterais dentro do cálculo, mas o tipo rank-2 de runST impede que referências mutáveis criadas internamente escapem
    runST :: (forall s. ST s a) -> a  
    
  • Internamente, comportamentos imperativos são possíveis, mas externamente só o resultado aparece, e o estado mutável não vaza para fora da fronteira
  • Esse princípio se aplica a todo o sistema operacional
    • A camada de banco de dados pode usar internamente pooling de conexões, retries e estado mutável
    • O cache pode usar mapas mutáveis concorrentes
    • O cliente HTTP pode ter circuit breakers, pools de conexão e bastante bookkeeping
    • O ponto central é encapsular comportamentos perigosos em interfaces estreitas para dificultar o uso indevido
  • Em sistemas reais, o objetivo não é evitar completamente mudanças, mas deixar claro onde elas estão e limitar quem, na base de código, precisa saber disso

Tornar a coisa certa a coisa fácil

  • Em bases de código grandes, frequentemente surgem padrões em que a correção depende de uma ordem específica ou de etapas extras invisíveis
    • É preciso dar flush no log de auditoria depois da transação
    • É preciso verificar a feature flag antes de chamar o endpoint
    • É preciso enfileirar a notificação dentro da transação do banco de dados
  • Se esse conhecimento operacional existir só em wikis, documentos de onboarding, reviews de design antigos, threads no Slack ou na memória de alguns engenheiros sêniores, ele desaparece rápido
  • Haskell permite codificar esses procedimentos em tipos para que não possam ser esquecidos
  • A forma ruim é pedir que usem a função correta, mas deixar um caminho de desvio disponível
    -- Please use this one, not the other one  
    writeWithEvents :: Transaction -> [Event] -> IO ()  
    
    -- Don't use this directly (but we can't stop you)  
    writeTransaction :: Transaction -> IO ()  
    publishEvents :: [Event] -> IO ()  
    
    • Uma forma melhor é reorganizar os tipos para que o único caminho de execução da operação inclua a publicação de eventos
    data Transact a -- opaque; cannot be run directly  
    record :: Transaction -> Transact ()  
    emit :: Event -> Transact ()  
    
    -- The *only* way to execute a Transact: commit and publish atomically  
    commit :: Transact a -> IO a  
    
  • Aqui, o sistema de tipos não está tanto provando um teorema profundo sobre eventos, mas tornando o procedimento operacional correto o caminho mais fácil
  • Quando um novo engenheiro pergunta como escrever uma transação, a assinatura de tipos e a API pública dão a resposta, e o conhecimento permanece mesmo quando engenheiros sêniores saem

Execução durável e Temporal

  • Os fluxos de trabalho de sistemas financeiros não ficam contidos em uma única transação
    • envio de pagamento
    • espera pela aprovação do parceiro
    • atualização do razão
    • notificação ao cliente
    • tratamento de cancelamentos e timeouts
    • casos em que o parceiro teve sucesso, mas o worker morreu antes de registrar
    • casos em que não há resposta por problemas de rede
  • Esses fluxos exigem estado, retries, timeouts, idempotência e execução durável que continue além de crashes de processo e deploys
  • No passado, a Mercury orquestrava esses processos com máquinas de estado baseadas em banco de dados, tarefas cron, workers em segundo plano e tratamento de retries e timeouts espalhado pelo código
    • Funcionava, mas era frágil, difícil de entender e uma causa desproporcional de incidentes operacionais
  • O Temporal é o framework de durable execution da Mercury, permitindo escrever workflows como código sequencial comum enquanto a plataforma registra cada etapa no histórico de eventos
  • Se um worker falha no meio de um workflow, outro worker pode fazer replay do prefixo determinístico para reconstruir o estado e continuar do ponto onde parou
  • Retries, timeouts, cancelamentos e tratamento de erros são fornecidos pela plataforma, em vez de serem reimplementados separadamente por cada equipe
  • Um workflow do Temporal tem uma natureza parecida com a de uma função pura sobre o histórico de eventos
    • o workflow reexecutado precisa produzir a mesma sequência de comandos do original
    • essa exigência de determinismo se parece com a restrição de mesmo input, mesmo output de código puro
    • efeitos colaterais são isolados em activities, que correspondem ao IO do workflow
  • A Mercury criou e abriu como open source o SDK Haskell hs-temporal-sdk, que encapsula o Core SDK oficial do Temporal com Rust FFI
  • O padrão de adoção do Temporal também foi abordado na palestra da conferência Temporal Replay, e a Mercury obteve melhorias operacionais ao substituir cadeias frágeis de cron e máquinas de estado por workflows duráveis

O domínio deve ser projetado na linguagem do negócio, não da camada de transporte

  • Um erro comum em sistemas que cresceram é deixar conceitos do sistema chamador vazarem para o modelo de domínio
  • Se um código escrito para handlers de requisições HTTP for depois reutilizado em tarefas cron, workers em segundo plano baseados em filas ou workflows do Temporal, exceções HTTP como StatusCodeException 409 "Conflict" podem se propagar para contextos não HTTP
  • Uma tarefa cron não tem um chamador esperando uma resposta 409, e o código de status leva o significado de negócio para a camada errada
  • A solução é modelar erros de domínio como tipos de domínio
    • saldo insuficiente deve ser InsufficientFunds
    • requisição duplicada deve ser DuplicateRequest
    • timeout do parceiro deve ser PartnerTimeout
  • Em cada fronteira, usa-se uma camada fina de transformação
    data PaymentError  
      = InsufficientFunds  
      | DuplicateRequest RequestId  
      | PartnerTimeout Partner  
    
    toHttpError :: PaymentError -> HttpResponse  
    toHttpError InsufficientFunds       = err402 "Insufficient funds"  
    toHttpError (DuplicateRequest _)    = err409 "Duplicate request"  
    toHttpError (PartnerTimeout _)      = err502 "Partner unavailable"  
    
    toWorkerStrategy :: PaymentError -> WorkerAction  
    toWorkerStrategy InsufficientFunds    = Fail "Insufficient funds"  
    toWorkerStrategy (DuplicateRequest _) = Skip  
    toWorkerStrategy (PartnerTimeout _)   = RetryWithBackoff  
    
  • As preocupações da camada de transporte devem ficar nas bordas, e o modelo de domínio não deve carregar códigos de status HTTP ao ser chamado de handlers web, CLI, tarefas cron, workers em segundo plano ou engines de workflow

O custo da codificação em tipos e o ponto de equilíbrio

  • Colocar invariantes nos tipos é poderoso, mas traz custos de carga cognitiva, rigidez e dificuldade quando os requisitos mudam
  • Se a violação puder levar a perda de dados, erro financeiro, problema regulatório ou incidente de on-call, o custo da codificação em tipos se justifica
  • Se a única razão for que o sistema funciona assim hoje, ou porque se quer experimentar técnicas de nível de tipo, há grande chance de tornar o codebase mais difícil de mudar
  • O lado de codificar demais

    • estados ilegais se tornam impossíveis de representar, e o domínio é modelado fielmente em tipos
    • mudanças em regras de negócio viram alterações de tipos que atravessam 50 módulos, alongando o refactor
    • fica mais difícil para novos engenheiros entenderem as assinaturas de tipos
  • O lado de não codificar nada

    • os tipos passam a se aproximar de String, IO () ou, no pior caso, Dynamic
    • o código fica fácil de mudar, mas sem contratos, e o significado depende da memória de quem o escreveu
    • quando a pessoa autora sai, fica difícil entender por que o sistema não funciona
  • Critérios úteis

    • invariantes que evitam corrupção silenciosa tendem a valer a pena nos tipos
      • transações confirmadas sem eventos
      • pagamentos processados sem log de auditoria
      • transições de estado aparentemente possíveis, mas semanticamente impossíveis
    • invariantes que falham de forma evidente podem ser suficientemente tratados com verificações em runtime e boas mensagens de erro
      • resposta 500
      • falha de assertion
      • incompatibilidade de tipos na fronteira JSON
    • o impulso de modelar todo o domínio em tipos deve ser contido
      • o domínio tem exceções, regras de compatibilidade legada, regras conflitantes e comportamentos especiais para clientes específicos
    • tipos são uma ferramenta para a equipe, não só para o compilador
      • eles devem formar uma camada de defesa junto com testes, documentação, code review, exemplos e playbooks
    • Dentro da Mercury também existem bibliotecas que usam mecanismos complexos de nível de tipo, como GADT, type family e phantom types que rastreiam transições de estado
    • essa complexidade é necessária em mecanismos onde, se algo der errado, o dinheiro pode ir para o lugar errado ou invariantes regulatórias podem ser quebradas
    • o essencial é encapsular a complexidade
    • o módulo que implementa a máquina de estados em nível de tipo deve ter poucos autores com entendimento profundo e testes suficientes
    • para quem usa, a API deve parecer algumas funções com tipos comuns
    • um product engineer deve conseguir chamar isso com segurança sem conhecer os mecanismos internos de prova em nível de tipo
    • se, em code review, um PR que mexe em outros módulos estiver cheio de anotações de tipo copiadas para apaziguar o compilador, isso é sinal de que a abstração está vazando para além da fronteira

Projetando para introspecção

  • Se confiabilidade é a capacidade de adaptação, introspecção é uma das formas de obter essa capacidade
  • Operadores não conseguem operar o que não conseguem ver, e equipes têm dificuldade para se adaptar a sistemas cujo interior é opaco
  • Haskell não tem monkey patching, então é difícil trocar em tempo de execução o cliente HTTP interno de uma biblioteca ou substituir chamadas ao banco de dados por funções que emitam spans do OpenTelemetry
  • Rust tem a mesma limitação, mas o ecossistema Rust convergiu para o padrão de middleware tower, enquanto o ecossistema Haskell se divide entre várias abordagens
  • Se uma biblioteca expõe apenas um conjunto de funções concretas de topo, para instrumentá-la é preciso encapsulá-la em um novo módulo e esperar que as pessoas importem esse módulo em vez do original
  • Registros de funções

    • A solução mais usada é expor registros de funções em vez de funções concretas
      -- A concrete module gives you no leverage:  
      sendRequest :: Request -> IO Response  
      -- A record of functions gives you all of it:  
      data HttpClient = HttpClient  
      { sendRequest :: Request -> IO Response  
      , getManager  :: IO Manager  
      }  
      
    • Com isso, é possível envolver sendRequest com instrumentação de tempo e retornar um novo HttpClient
    • Dá para adicionar em tempo de execução preocupações transversais como fault injection para testes, troca por mocks, retries, tracing, rewrite de requisições e comportamento por tenant
    • Como em type Middleware = Application -> Application do WAI, esse padrão que torna transformações de comportamento componíveis é muito útil operacionalmente
  • Interceptadores compostos com Monoid

    • Tipos de middleware e interceptador em geral podem ter instâncias de Semigroup e Monoid
    • O Middleware do WAI é um endomorfismo, e endomorfismos formam um monoid sob composição e id
    • Registros de hooks de interceptadores podem ser compostos campo a campo, então preocupações como tracing, timeout e rewrite de fila de tarefas podem ser combinadas com mconcat sem encanamento adicional
      appTemporalInterceptors =  
      mconcat  
        [ retargetingInterceptor  
        , otelInterceptor  
        , sentryInterceptor  
        , sqlApplicationNameInterceptor  
        , loggingContextInterceptor  
        , statementTimeoutInterceptor  
        , teamNameInterceptor  
        , clientExceptionInterceptor  
        , workflowTypeNameInterceptor  
        ]  
      
    • Cada interceptador lida com apenas uma preocupação em um módulo independente, sobrescreve apenas os campos necessários a partir de mempty, e a ordem fica explícita na lista
  • Sistemas de efeitos

    • Sistemas de efeitos como effectful, polysemy, fused-effects e cleff também oferecem outro caminho
    • Você define as operações disponíveis como tipos de efeito e pode trocar no ponto de chamada os interpreters de produção, teste e tracing
    • É possível interceptar efeitos, registrar métricas ou injetar latência, e então reenviá-los ao handler real
    • A desvantagem é introduzir mecanismos extras como listas de efeitos no nível de tipos, pilhas de handlers e erros de tipo difíceis
    • Registros de funções são simples o suficiente para um novo engenheiro entender em uma tarde
  • Um exemplo positivo de persistent

    • O SqlBackend de persistent é um registro de funções com itens como connPrepare, connInsertSql, connBegin, connCommit e connRollback
    • Ao adicionar instrumentação do OpenTelemetry, foi possível envolver os campos relevantes e anexar spans de tracing a todas as operações de banco de dados
    • Sem fork e com quase nenhuma mudança no código-fonte, obteve-se visibilidade da camada de banco de dados
  • Bibliotecas difíceis de operar

    • A Mercury quase não usa bindings públicos de clientes de API web publicados no Hackage
    • Quando um binding de terceiros faz chamadas HTTP com funções concretas, fica difícil fazer tracing, aplicar timeouts alinhados ao SLO, simular falhas de parceiros ou explicar um vazio de 400 ms no trace
    • Por isso, a empresa escreve seus próprios clientes e os torna observáveis desde o início
  • O custo de um ecossistema pequeno

    • Algumas bibliotecas Haskell não estão abandonadas, mas acabam como infraestrutura pública sem um responsável claro que faça melhorias rápidas
    • Interfaces antigas são mantidas, e a adoção de novos designs sobre observabilidade, modelagem de fronteiras e operabilidade pode ser lenta
    • http-client suporta diretamente apenas HTTP/1.1; é utilizável o bastante, mas em certos momentos pode exigir contornos

Requisitos operacionais para autores de pacotes

  • Autores de bibliotecas devem fornecer pontos de escape como registros de funções, tipos de efeito e callbacks para que usuários possam injetar comportamento sem modificar o código-fonte
  • Só de adicionar hs-opentelemetry-api como dependência e colocar spans em torno das operações centrais de IO já ajuda quem opera a biblioteca em produção
    • O pacote de API é conservador com breaking changes e foi projetado para funcionar de forma inerte se a aplicação não inicializar o SDK do OpenTelemetry
    • O overhead de desempenho é mínimo, e ele não gera exceções inesperadas nem logging a partir da aplicação do usuário
    • O footprint de dependências ainda não é tão pequeno quanto se gostaria, e há trabalho em andamento para melhorar isso
  • Não se deve escrever logs diretamente no código da biblioteca
    • Em vez de importar um framework de logging e escrever direto em stdout ou stderr, é melhor fornecer callbacks, parâmetros de logger ou um tipo de dado de mensagem de log que o chamador possa rotear
    • Para onde os logs vão é uma decisão que pertence ao ambiente operacional da aplicação
    • A Mercury envia pipelines de logs estruturados para a stack de observabilidade; se a biblioteca escrever diretamente em stderr, isso exigirá encanamento separado do fluxo de JSON lines
  • Também vale considerar expor módulos .Internal
    • A preocupação de que usuários passem a depender de APIs internas e dificultem refactors é válida
    • Mas raramente se justifica a certeza de que a API pública já cobre todos os casos de uso
    • Um módulo .Internal com aviso explícito de estabilidade pode ser melhor do que o usuário fazer fork do pacote e vendoring
    • containers, text e unordered-containers são bons exemplos dessa abordagem no ecossistema Haskell
    • Por outro lado, se usuários resolverem silenciosamente suas necessidades usando módulos internos, pode haver menos feedback sobre falhas na API pública

O que não colocamos nos tipos

  • Mesmo Haskell em produção tem partes nada elegantes
  • unsafePerformIO é usado dentro de bibliotecas das quais dependemos no dia a dia
    • bytestring e text alocam buffers mutáveis internamente, escrevem neles e depois fazem freeze para produzir o resultado
    • O tipo não diz o que aconteceu durante a construção
    • Os limites são mantidos por convenção, raciocínio cuidadoso e revisão de código
  • Se a alternativa type-safe tornar o custo de desempenho ou complexidade excessivo, você mesmo pode acabar escrevendo esse tipo de compromisso
    • É preciso documentar os invariantes que o sistema de tipos não verifica
    • É preciso manter o desconforto e reavaliar periodicamente se uma alternativa type-safe se tornou prática
    • Haskell em produção não é ausência de compromissos, e sim isolamento disciplinado dos compromissos
  • Muitas bibliotecas Haskell no Hackage têm poucos testes ou nenhum
    • A ideia de que “se compila, funciona” às vezes pode ser verdadeira para código puro pequeno e tipos fortes
    • Quase nunca é verdade para código pesado em IO, integrações com sistemas externos e código em que os bugs estão no significado, não na estrutura
  • Os tipos podem dizer que algo retorna Either ParseError Transaction, mas não conseguem dizer o seguinte
    • se o campo amount é analisado em centavos ou em dólares
    • se a API do parceiro interpreta de forma diferente um campo omitido e um campo nulo
    • se a lógica de retry causa cobrança em duplicidade em uma janela específica de tempo de ano bissexto
  • Em produção, construímos sistemas sobre essas bibliotecas e herdamos suposições não verificadas, então precisamos complementar isso com testes de integração na nossa própria camada
  • Também se acumulam compromissos como orphan instance, funções parciais acreditadas como totais no contexto, error prometido como inalcançável, wrappers de FFI estranhos e hierarquias de exceção feitas manualmente
  • O objetivo não é pureza moral, e sim garantir, por meio de revisão de código, documentação, exemplos e testes, que seja possível saber onde está cada compromisso, por que ele foi feito e o que quebra se ele for removido

Vale a pena usar Haskell em produção

  • Haskell não é a escolha mais rápida no primeiro dia
    • O ecossistema atual não oferece de imediato um ambiente de desenvolvimento com hot reloading e tudo incluído como Next.js ou Rails
    • A biblioteca necessária pode não existir, ou existir mas ser mantida por uma única pessoa no tempo livre
    • Às vezes as mensagens de erro são extremamente difíceis de entender
  • O problema de contratação é exagerado
    • O CTO da Mercury, Max Tagher, já disse publicamente que engenheiro de backend Haskell é o cargo mais fácil de contratar em toda a Mercury
    • A demanda por empregos em Haskell é maior que a oferta, invertendo a dinâmica normal de contratação
    • A Mercury contrata tanto pessoas com muita experiência em Haskell quanto pessoas sem nenhuma, e estas últimas passam a ser produtivas com um programa de treinamento de 6 a 8 semanas
    • Se você precisasse de 100 especialistas em Haskell amanhã, o problema do tamanho do pool seria real; mas, se estiver disposto a contratar bons desenvolvedores generalistas e ensiná-los, ele é menos real
  • O maior risco de contratação não é o tamanho do pool, e sim a inclinação
    • Haskell atrai idealistas que se importam com precisão e abstração, gostam de ler artigos acadêmicos e questionam suposições existentes
    • Se essa força não for controlada, pode virar um passivo em produção
    • Tentar reescrever a camada de banco de dados com uma nova codificação de álgebra relacional em nível de tipos, recusar merge porque um script descartável usou String em vez de Text, ou puxar todo design para um total rewrite no estilo do artigo mais recente desacelera o time
  • Haskell em produção exige uma cultura de pragmatismo
    • O sistema de tipos é uma ferramenta elétrica, não uma religião
    • Tratar problemas que já têm boas soluções como oportunidades de inventar um novo mecanismo não combina com produção
  • O retorno aparece com o tempo
    • Uma refatoração que levaria semanas em uma base de código com tipagem dinâmica pode terminar em horas depois de uma mudança de tipo, porque o compilador aponta todos os call sites
    • Um novo engenheiro pode ler as type signatures e entender o contrato de um módulo
    • Estados impossíveis podem de fato se tornar impossíveis de representar, evitando incidentes em produção
  • A Mercury vê o retorno sobre o investimento aparecer em meses, não em anos
    • Especialmente em serviços financeiros, o custo de bugs de integridade de dados não é medido em reclamações de usuários, e sim em questionamentos regulatórios e no dinheiro dos outros
    • O sistema de tipos não elimina o risco, mas fornece ferramentas que tornam mais difícil introduzi-lo por engano em uma base de código que cresce rápido
  • O valor de Haskell em produção não está em ser bala de prata nem movimento moral, e sim em um conjunto poderoso de ferramentas que permite até a equipes com diferentes níveis de domínio de Haskell manter dispositivos perigosos dentro de limites, preservar conhecimento operacional e fazer do caminho seguro o caminho fácil

1 comentários

 
GN⁺ 2 시간 전
Comentários do Hacker News
  • É verdade que Haskell está entre as linguagens mais fortes para impor esse tipo de coisa via tipos, mas o mesmo padrão funciona muito bem também em Rust e TypeScript
    Também gosto da forma de evitar bugs óbvios de autorização que se repetem em apps web com um fluxo como User -> LoggedInUser -> AccessControlledLoggedInUser
    Acho que esse padrão é usado muito menos do que deveria na indústria

    • Isso não vale só para Rust ou TypeScript; na prática, é possível em quase qualquer linguagem
      Se for preciso distinguir, por segurança, strings antes/depois de escaping, até em linguagens dinâmicas dá para encapsular com uma classe Escaped e ter funções como escape(str)->Escaped, dangerouslyAssumeEscaped(str)->Escaped
      Há um custo de desempenho, então é preciso fazer concessões, mas é possível
      Outra abordagem é Application Hungarian, embora aí se dependa mais da disciplina do programador do que do compilador: https://www.joelonsoftware.com/2005/05/11/making-wrong-code-...
    • Isso está mais próximo de um problema de affordance do que do sistema de tipos em si
      Por exemplo, em C# isso também é plenamente possível, mas na prática acaba gerando mais ruído visual do que a própria definição dos tipos
    • Rust e TypeScript claramente também foram muito influenciados por Haskell
      Só que, para evitar o efeito “mônadas são assustadoras, então vamos escrever um tutorial”, costumam não dizer isso explicitamente e até dar outros nomes às coisas
      A influência maior talvez venha mais de type classes do que de mônadas
    • Não tenho tanta certeza de que isso funciona realmente bem em TypeScript
      Como não há tipagem nominal, para criar algo como um newtype que encapsule tipos primitivos é preciso lembrar de uns truques bem hacky
      Pela minha experiência, OCaml foi mais poderoso que Rust para impor esse tipo de segurança de tipos
      Com GADTs ele tem mais poder de expressão, além de mais conveniência com variantes polimórficas e tipos de objeto/row types de registros, sem falar no sistema de módulos e funtores
      E, em domínios onde garbage collection basta, também se evitam as limitações de abstração e as dificuldades causadas pelo borrow checker do Rust
    • É a mesma ideia de “tornar estados inválidos impossíveis de representar”: https://news.ycombinator.com/item?id=40150159
  • Gostei muito de trabalhar com Haskell por alguns anos
    Não foi algo que eu estivesse buscando de propósito, mas a oportunidade apareceu por acaso, e foi interessante e intelectualmente estimulante
    Só que, infelizmente, mesmo depois de 3 anos usando apenas Haskell, minha produtividade em Rust ainda era facilmente o dobro da que eu tinha em Haskell
    Haskell tem mais armadilhas que você precisa conhecer de antemão para evitar, e dependendo de quem escreve, o código pode ser tão difícil de absorver que vira quase uma linguagem só de leitura
    O toolchain frequentemente vem acoplado com Nix, que por si só já é um monstro complexo, as extensões da linguagem parecem espalhadas por todo lado
    Os arquivos Cabal também não são grande coisa, e leva tempo para se acostumar com os erros do compilador

    • Curiosamente, minha experiência foi quase o oposto
      No meu último produto, comecei a migrar o backend de Typescript para Rust porque eu estava cansado dos crashes
      Hoje considero isso um dos maiores erros técnicos que já cometi, porque a produtividade despencou
      Um exemplo de tempo perdido só em Rust: escrever uma função de ordem superior do tipo “abre conexão com banco de dados, faz algo e fecha” é trivial em Haskell, TypeScript, JavaScript, C++ e PHP, mas em Rust foi tão impraticável que, mesmo perguntando a amigos especialistas em Rust, acabei desistindo
      Também houve várias vezes em que tentei refatorar, passei o dia inteiro corrigindo erros de tipo, só para chegar a um erro no arquivo de topo e perceber que, por uma parte fundamental do design, a refatoração inteira era inviável e precisava ser revertida
      Além disso, Rust é a única linguagem moderna que consigo lembrar em que usar valores por interface em vez de tipos concretos fica, dependendo do caso, em algum lugar entre técnica avançada e simplesmente impossível
      Por isso cheguei à conclusão de que código de aplicação, isto é, código que não seja de sistema nem de biblioteca, basicamente não deveria ser escrito em Rust
    • Fico curioso se a produtividade foi 2x maior de forma geral, ou se também houve partes em que Rust foi menos produtivo
      E também queria entender o que você quis dizer com “só de leitura”
  • Ao contrário da percepção geral, acho que o fato de a Mercury ter escolhido Haskell, e de seus líderes iniciais terem ampla experiência com Haskell, pode muito bem ter contribuído de forma importante para o sucesso
    Do ponto de vista de cliente da Mercury, essa empresa é uma das peças centrais da minha caixa de ferramentas, e não consigo afastar a sensação de que a escolha por Haskell tornou melhores o progresso, o desenvolvimento e a jornada como um todo
    Claro que dá para fazer esse tipo de afirmação sobre a maioria das linguagens, e isso não significa que linguagens funcionais como Haskell sejam uma fórmula de sucesso
    Mas tomar esse tipo de decisão deliberada antes da era do “vibe coding” e dos LLMs parece especialmente visionário, e vejo isso como resultado combinado com a cultura de engenharia detalhada no texto

    • É mais provável que o fator de sucesso tenha sido o foco em fintech orientada a startup e a capacidade de execução
      Eu também gosto de boa cultura técnica, mas já vi empresas com ótima cultura técnica morrerem por causa de um foco de negócio ruim
      Indo além, pode até ser que uma cultura de fintech no estilo startup tenha produzido uma boa cultura técnica
      Como eles não começaram como banco, ao contrário do SVB por exemplo, não precisavam ser tão conservadores nem integrar com stacks tecnológicos antigos e horríveis
      Fico feliz que tenham tido sucesso com Haskell, mas, como Jane Street com OCaml, acho que, ao contrário do que a empresa talvez queira fazer acreditar, a escolha da linguagem é quase acidental do ponto de vista do negócio
      Só fico curioso sobre o que usam no frontend. Imagino que esse Haskell seja todo backend
    • Contratar generalistas sem experiência naquela linguagem específica também pode ter ajudado
      Porque assim dava para incutir cultura e estilo desde o começo nas pessoas que chegavam
      Antes do vibe coding, a maioria dessas pessoas provavelmente não entraria simplesmente saindo hackeando tudo sem instrução
    • A sensação é que tudo no app simplesmente funciona
      Quando você vem de outros serviços, isso é realmente muito satisfatório
  • Um amigo muito próximo trabalha nessa empresa, e mesmo de fora a cultura de engenharia parece boa
    Acho que Haskell é a ferramenta certa para esse trabalho e que eles estão aproveitando bem seus pontos fortes, mas também fico com a impressão de que boa parte do sucesso pode vir simplesmente do fato de a empresa ser bem administrada no geral

    • Foi essa a sensação que tive lendo o texto
      Parece que esse autor conseguiria tocar uma organização de engenharia bem-sucedida independentemente da linguagem usada
    • Isso também não contradiz a ideia comum de que usar uma linguagem de programação funcional filtra um conjunto melhor de talentos/candidatos
  • Estou lendo Real-World OCaml agora e, embora já soubesse de algumas coisas, estou aprendendo mais programação funcional
    Parece possível construir componentes de software surpreendentemente robustos com programação funcional
    Mas também fico em dúvida
    Hoje o backend do produto roda com NiceGUI e cumpre bem o seu papel
    O código é razoável, em MVVM, e a coisa mais importante é conectar no websocket por cliente para consumir dados e mostrar análises
    Não deve haver muitos clientes, e o site provavelmente terá de algumas dezenas até, no máximo, algumas centenas de visitantes
    Também quero REPL ou hot reload, e sei que, conforme as funcionalidades crescerem, programação funcional pode combinar bem com transformações de pipeline de dados em coisas como painel de gerenciamento de usuários, mais análises etc.
    Só que Haskell e OCaml são linguagens estáticas
    Se eu quiser algo dinâmico que também cresça e escale depois, Clojure ou Elixir parecem boas escolhas
    Ao mesmo tempo, tenho medo de que, se um dia for preciso refatorar, tudo quebre
    Hoje uso Python com Mypy, e o frontend é gerado pelo backend via NiceGUI

    • Não conheço OCaml, mas em Haskell dá para recarregar muito rapidamente um app web em desenvolvimento com ghci/cabal repl
      Sinceramente, acho que muitos usuários de Haskell não aproveitam isso o suficiente
  • Já trabalhei em um sistema parecido usando uma linguagem relativamente de nicho, Scheme e depois Racket, e mesmo com o crescimento ele continuou sendo mantido por um time pequeno por muito tempo, mantendo boa velocidade
    Não produzíamos muitos bugs e normalmente conseguíamos adicionar funcionalidades muito rápido
    Por exemplo, fomos os primeiros a conseguir uma certa certificação para hospedar dados sensíveis na AWS
    Às vezes, adicionar recursos ficava mais lento porque, em vez de usar componentes prontos das plataformas populares, precisávamos construir coisas do zero
    Mas, uma vez feito, funcionava bem, voltávamos ao ritmo anterior e não ficávamos lentos por causa da gordura e da complexidade de dezenas de frameworks prontos
    Como tínhamos controle direto sobre uma plataforma administrável, também conseguíamos migrar rapidamente para a AWS quando surgiu necessidade
    Desde o início, o sistema também tinha um certo segredo arquitetural para lidar com dados complexos e interações web, e isso permitia desenvolver muitas funcionalidades rapidamente, além de continuar empurrando o sistema em direções inteligentes depois
    A diferença em relação à fintech em Haskell é que o tamanho do time era muito pequeno
    Em qualquer momento havia só 2 ou 3 engenheiros de software, além de uma pessoa responsável por toda a operação
    Então não existia a dificuldade de coordenar centenas de pessoas mantendo um sistema consistente
    Normalmente uma pessoa cuidava das mudanças de código mais técnicas e arquiteturais, e outra adicionava rapidamente funcionalidades cheias de lógica de negócio para processos complexos
    Se usadas com cuidado, as ferramentas de IA do tipo LLM atuais ou de futuro próximo talvez permitam capturar parte da eficiência de times muito pequenos e extremamente eficazes também no desenvolvimento de software
    O modelo que me vem à cabeça não é o de produzir um gigantismo inchado só para eliminar story points e empurrar a sustentabilidade para depois, mas sim o de poucos pensadores muito afiados mantendo o sistema em um caminho ao mesmo tempo poderoso e administrável

  • É uma faca de dois gumes
    2 milhões de linhas é uma conquista impressionante, mas também um peso de manutenção considerável
    As vantagens de Haskell são teoricamente claras, mas as desvantagens são menos intuitivas
    A tentação está em modelar tudo nos tipos
    A própria base de código deixa de ser uma aplicação e passa a ser uma especificação de negócio
    Cada mudança de política vira uma grande refatoração e, graças à segurança do Haskell, isso pode acabar surpreendentemente trabalhoso
    No fim, não dá para ter tudo, e em algum momento você acaba preso aos tipos
    Haskell é realmente impressionante e poderoso, especialmente nessa escala, mas traz problemas próprios
    A tentação de modelar lógica de negócio nos tipos pode criar estruturas rígidas, e a segurança fornecida por essa estrutura pode fazer você deixar de enxergar outros tipos de risco

    • Engenheiros experientes e de bom gosto, se construírem as partes centrais, conseguem equilibrar isso muito bem
      Não dá para ter tudo, mas dá para ter muita coisa
      Fiz estágio na Jane Street alguns anos atrás; não era Haskell, era OCaml, mas parecia que eles encontravam esse equilíbrio muito bem
      Mesmo em um domínio com alta complexidade intrínseca, onde confiabilidade e precisão estão diretamente ligadas à sobrevivência do negócio, eles se moviam com velocidade surpreendente
      Olhando para trás, o centro da força da Jane Street estava em contratar programadores de OCaml experientes e com ótimo senso, como Stephen Weeks, e colocá-los para construir as bibliotecas centrais desde o começo e conduzir toda a base de código
      Infelizmente, a Mercury parece não ter feito essa parte tão bem assim
    • O mesmo vale para TypeScript: https://www.richard-towers.com/2023/03/11/typescripting-the-...
      Sinceramente, o maior problema de um sistema de tipos Turing-completo é que, em teoria, dá para implementar uma aplicação inteira que, ao compilar, vira pó
  • Um caso parecido de sucesso com Haskell na Bellroy será tema de um encontro Melbourne Compose em breve: https://luma.com/uhdgct1v

  • O problema que eu tenho com programação funcional é debugging
    Mais exatamente, eu vejo isso como um ponto forte da programação imperativa, especialmente no estilo procedural
    Em estilo funcional/declarativo, em geral você descreve não como algo é construído, mas que estado aquilo deveria ter, e a linguagem monta tudo e entrega o resultado final
    Se você fez tudo certo, ótimo, talvez até melhor; mas, quando isso não acontece e o resultado esperado não aparece, a questão é como encontrar o bug
    Em linguagens como C isso é relativamente simples
    Você segue linha por linha, observa o estado de execução entre cada etapa, na prática a RAM, e se algo divergir do esperado então há algo errado naquela linha, então você entra ali e segue assim
    Quanto mais a programação funcional tenta esconder o estado, mais difícil isso fica
    Também é interessante que a seção mais longa do texto trate justamente desse problema, o tal “design for introspection”
    O autor precisou fazer um esforço deliberado para tornar o código depurável, e isso traz uma boa visão prática de um uso de Haskell que muitas vezes é ignorado

    • Meu truque de debugging é fazer com que todo código minimamente importante retorne a mesma saída para a mesma entrada
      Até código trivial também
      As outras linguagens mainstream nem chegam perto disso
      Em situações em que não dá para escrever assim, como concorrência com memória compartilhada, eu uso transações
      E nisso também as outras linguagens mainstream não chegam perto
      Nem estou contando vantagens fáceis como ausência de null, ausência de casts implícitos de inteiro etc.
      É totalmente verdade que depurar código Haskell é mais difícil do que em outras linguagens
      Mas isso é inevitável quando você elimina os 90% inferiores das armadilhas que atrapalham o caminho
    • O debugging em programação funcional, ao contrário da imperativa, muitas vezes é orientado a REPL
      Claro, isso não é exclusivo do funcional; mesmo em linguagens majoritariamente imperativas como Python e JavaScript, muita gente usa o shell do Python, o console do navegador, shells de Node/Deno/Bun, notebooks etc. como primeira camada de debugging
      Há concessões interessantes nesse modelo centrado em REPL
      Em linguagens como C, costuma-se partir do debugging do programa inteiro e de breakpoints, tentando acertar o ponto exato onde pode estar o problema
      Num mundo centrado em REPL, tenta-se tornar mais possível testar diretamente no REPL os componentes do programa
      Por isso, as fronteiras de módulo/API/tipo acabam se parecendo com as fronteiras de depurabilidade
      Às vezes há uma pressão maior para tornar essas fronteiras corretas e fáceis de usar do que em linguagens imperativas como C/C++
      Em compensação, comparado ao debugging centrado no programa inteiro, pode ficar mais difícil isolar problemas complexos de integração entre unidades em cenários estranhos do mundo real
      Mas a abordagem REPL-first muitas vezes incentiva a reduzir ao mínimo a área de superfície de integração, então em linguagens funcionais esses efeitos de integração podem aparecer menos do que em linguagens imperativas
      Não acho correta a ideia de que linguagens funcionais escondem estado
      Elas também rodam em hardware imperativo e lidam com estado real do hardware
      Em algum ponto existe uma tradução entre esses dois mundos, e talvez ela nem seja tão diferente quanto parece
      Quando necessário, ainda dá para voltar a breakpoints e depuradores imperativos
      Por isso eu chamo de debugging “orientado a REPL”
      Com o REPL você consegue afunilar o problema até a unidade com defeito, isto é, o módulo/API/função exato e as entradas que produzem uma saída surpreendente
      Se olhando o código-fonte o bug ainda não ficar visível, dá para passar para o debugger imperativo e ter praticamente a mesma experiência de executar linha por linha, além de ganhar contexto extra
      Nesse ponto, você provavelmente já afunilou tanto com o REPL que a própria unidade é pequena e estreita, então talvez nem precise mais escolher bons breakpoints
      Acho que a mensagem captada da seção “design for introspection” foi equivocada
      Aquela seção não era sobre depurabilidade, mas sobre observabilidade
      Era sobre conectar corretamente sistemas de logging/telemetria, mockar fakes durante testes e adicionar retry/circuit breakers no nível do sistema inteiro, em vez de deixar isso a cargo de bibliotecas isoladas
      Mesmo no mundo imperativo, isso não é um problema de debugging, e sim de decomposição via injeção de dependência, instalação de middleware e uso de interfaces abstratas em vez de classes concretas nas fronteiras de API pública
      Esse tipo de sugestão de design é refatoração, e afeta menos a depurabilidade do que o quão fácil é instalar middleware de observabilidade na API pública de outra pessoa
  • Acho difícil imaginar o que exatamente 2 milhões de linhas de Haskell estariam fazendo
    É muito código, e Haskell me passa a impressão de ser uma linguagem “densa”, capaz de fazer muita coisa com pouco código
    Fico pensando se isso vem de haver muitas bibliotecas para coisas como serialização/desserialização JSON, frameworks de API REST, logging etc.

    • Segundo o texto original, o problema é que código que não pode ser instrumentado não merece confiança
      Se bindings de terceiros fazem chamadas HTTP com funções concretas, não há como adicionar tracing, não há como injetar timeouts alinhados aos SLOs, não há como simular falhas de parceiros nos testes, nem como explicar um buraco de 400ms no trace sem ficar só teorizando
      Então eles escrevem isso por conta própria
      Dá mais trabalho no começo, mas os clientes que constroem já nascem projetados para observabilidade
    • A característica que você chamou de “densa” costuma ser chamada de alta expressividade
      Quer dizer que ideias relativamente muito abstratas podem ser expressas com poucos caracteres
      Algumas pessoas também chamam isso de “alto nível”
      Ainda assim, acho que 2 milhões de linhas não é tanto código quanto parece à primeira vista
      Especialmente para uma empresa em um setor regulado como o financeiro e com código acumulado ao longo de anos
    • Não é nenhuma métrica objetiva, mas sempre senti que Haskell simplesmente tem uma proporção diferente
      O número de linhas pode até ser um pouco menor, mas a contagem de palavras costuma ser parecida com a de linguagens orientadas a objetos mais imperativas
    • Não sei como essa base de código é de fato, mas a reputação de concisão do Haskell é em parte exagerada por uma sobrerepresentação do meio acadêmico e da teoria das categorias
      Nesse meio, algo como St M -> C T até pode parecer aceitável, mas em software real é muito mais útil escrever TransactionState Debit -> Verified Transaction
      Outra parte vem de um fator cultural que remonta ao LISP
      Existe uma tendência de algumas pessoas tentarem economizar linhas com truques difíceis de entender ou macros, sendo excessivamente espertas
      Numa empresa financeira como a Mercury, imagino que clareza e legibilidade sejam mais incentivadas do que esse tipo de prática
      Por exemplo, um linter poderia forçar a quebrar código monádico em expressões do detalhadas com várias linhas, em vez de escrever tudo em uma linha com >> e >>=