Fenner
A linguagem

Fenner.Result e a propagação F()?

Erros como valor (TResult) em vez de exceção, e o operador ? que repassa a falha para quem chamou.

O que é e por que existe#

Nem todo erro merece uma exceção. "Divisão por zero", "arquivo não achado", "texto inválido" são resultados esperados; uma exceção interrompe o fluxo de quem nem ia tratá-la. TResult<T, E> representa "deu certo com um valor T" ou "falhou com um erro E" como um valor comum, e o compilador ajuda a não ter de escrever o if falhou então sai em cada passo.

Fenner
uses Fenner.Result;                 // vem de Compiler\extras (ponha no SourcePath)

type
  TIntResult = TResult<Int32, UString>;

function Divide(A, B: Int32): TIntResult;
begin
  if B = 0 then
    Exit(TIntResult.Fail('divisão por zero'));
  Result := TIntResult.Ok(A div B);
end function;

O record oferece IsOk, IsFail, Value, Error, Ok/Fail (funções de classe), Unwrap, UnwrapOr, UnwrapErr e Map. Unwrap de uma falha levanta exceção.

O operador ? posfixo#

Fenner
function HalfOfQuotient(A, B: Int32): TIntResult;
var
  LQuot: Int32;
begin
  LQuot := Divide(A, B)?;           // falhou: ESTA função sai com a mesma falha
  Result := TIntResult.Ok(LQuot div 2);
end function;
  • X := F()?; avalia F(). Se IsOk é falso, a função atual faz Exit(<falha>) (um Exit comum: finally, ARC e variáveis locais são tratados como em qualquer saída). Se é verdadeiro, o valor da expressão é o Value.
  • Vale em qualquer posição de expressão (argumento, condição, operando) e como instrução (F()?;). A ordem é a do fonte: em A() + F()? + B(), o B só é chamado se F deu certo.
  • Vale para qualquer record com os campos IsOk (Bool8), Value e Error, não só o TResult.
  • Se a função devolve outro tipo Try, a falha é refeita com Tipo.Fail(erro): o tipo devolvido precisa de uma class function Fail(const AError: E) com o mesmo E. Não há conversão de erro.
Propagate.fxp
program Propagate;
// Demo: Fenner.Result (TResult<T, E>) e a propagação `F()?`.
// Em vez de exceções, a função devolve "ok com valor" ou "falha com erro".
// O `?` posfixo repassa a falha para quem chamou, sem if/Exit manual a cada passo.

uses Fenner.Result;     // vem de Compiler\extras; o Propagate.config põe essa pasta no SourcePath

type
  TIntResult = TResult<Int32, UString>;

function Divide(A, B: Int32): TIntResult;
begin
  if B = 0 then
    Exit(TIntResult.Fail('divisão por zero'));
  Result := TIntResult.Ok(A div B);
end function;

// Sem `?`: é preciso testar cada resultado à mão.
function ManualHalfOfQuotient(A, B: Int32): TIntResult;
var
  LQuot: TIntResult;
begin
  LQuot := Divide(A, B);
  if LQuot.IsFail then
    Exit(LQuot);
  Result := TIntResult.Ok(LQuot.Value div 2);
end function;

// Com `?`: se Divide falhar, ESTA função já sai devolvendo a mesma falha;
// se der certo, a expressão vale o Value.
function HalfOfQuotient(A, B: Int32): TIntResult;
var
  LQuot: Int32;
begin
  LQuot := Divide(A, B)?;
  Result := TIntResult.Ok(LQuot div 2);
end function;

// O `?` vale em qualquer posição de expressão e encadeia várias etapas.
function Chain(A, B, C: Int32): TIntResult;
begin
  Result := TIntResult.Ok(Divide(A, B)? + Divide(A, C)?);
end function;

procedure Show(const ALabel: UString; const AResult: TIntResult);
begin
  if AResult.IsOk then
    WriteLn(ALabel, ': ok ', AResult.Value)
  else
    WriteLn(ALabel, ': falha "', AResult.Error, '"');
end procedure;

var
  R: TIntResult;
begin
  Show('manual 20/2/2', ManualHalfOfQuotient(20, 2));
  Show('manual 1/0', ManualHalfOfQuotient(1, 0));
  Show('com ? 20/2/2', HalfOfQuotient(20, 2));
  Show('com ? 1/0', HalfOfQuotient(1, 0));
  Show('soma 12/3 + 12/4', Chain(12, 3, 4));
  Show('soma 12/3 + 12/0', Chain(12, 3, 0));      // a segunda divisão falha
  // utilitários do TResult
  R := Divide(8, 2).Map(function(const V: Int32): Int32; begin Result := V * 10; end);
  WriteLn('Map: ', R.Value, '  UnwrapOr em falha: ', Divide(1, 0).UnwrapOr(-1));
  try
    WriteLn(Divide(1, 0).Unwrap);                  // Unwrap em falha levanta exceção
  except
    on E: Exception do WriteLn('exceção: ', E.Message);
  end try;
end.
Propagate.config
Platform MSWin64
Default Platform MSWin64
Mode Debug
Default Mode Debug
PlatformMode All * *
PlatformMode MSWin64Debug MSWin64 Debug
Config All SourcePath ($FcPath)\..\extras
Saída
manual 20/2/2: ok 5
manual 1/0: falha "divisão por zero"
com ? 20/2/2: ok 5
com ? 1/0: falha "divisão por zero"
soma 12/3 + 12/4: ok 7
soma 12/3 + 12/0: falha "divisão por zero"
Map: 40  UnwrapOr em falha: -1
exceção: TResult.Unwrap em um resultado de falha

Demo 44. A versão manual e a versão com ?; soma de duas divisões; Map, UnwrapOr e Unwrap.

Erros típicos#

CódigoQuando
FE2400? fora de uma function (programa principal, procedure)
FE2401? dentro de um bloco finally (o Exit não pode sair dele)
FE2402o retorno da função não é o mesmo tipo nem um tipo Try com Fail(const E)
FE2403função com mais de um retorno (function F: A, B)
FE2404? posfixo depois de um tipo que não é Try

Outras notas:

  • O ? do ternário (C ? A : B), o ?X (tem valor) e o tipo anulável ?T não mudam: o ? posfixo só age quando o tipo à esquerda é um tipo Try.
  • F()?.Campo é (F()?).Campo; não conflita com o ?. do acesso condicional, que continua desligado por padrão.
  • Map não muda o tipo (T → T).
  • Fenner.Result mora em Compiler\extras, fora da RTL: o .config precisa de Config All SourcePath apontando para lá, como o do exemplo acima.
  • Um TResult escrito como choice também é possível (Tipos soma), mas o Fenner.Result é o oficial.

Veja também#

  • Nulos e exceções, para o outro estilo de tratar erros.
  • Demo 44-result-propaga; manual try-propagate.md.