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.
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#
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()?;avaliaF(). SeIsOké falso, a função atual fazExit(<falha>)(umExitcomum:finally, ARC e variáveis locais são tratados como em qualquer saída). Se é verdadeiro, o valor da expressão é oValue.- Vale em qualquer posição de expressão (argumento, condição, operando) e como instrução (
F()?;). A ordem é a do fonte: emA() + F()? + B(), oBsó é chamado seFdeu certo. - Vale para qualquer record com os campos
IsOk(Bool8),ValueeError, não só oTResult. - Se a função devolve outro tipo Try, a falha é refeita com
Tipo.Fail(erro): o tipo devolvido precisa de umaclass function Fail(const AError: E)com o mesmoE. Não há conversão de erro.
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.Platform MSWin64
Default Platform MSWin64
Mode Debug
Default Mode Debug
PlatformMode All * *
PlatformMode MSWin64Debug MSWin64 Debug
Config All SourcePath ($FcPath)\..\extrasmanual 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 falhaDemo 44. A versão manual e a versão com ?; soma de duas divisões; Map, UnwrapOr e Unwrap.
Erros típicos#
| Código | Quando |
|---|---|
FE2400 | ? fora de uma function (programa principal, procedure) |
FE2401 | ? dentro de um bloco finally (o Exit não pode sair dele) |
FE2402 | o retorno da função não é o mesmo tipo nem um tipo Try com Fail(const E) |
FE2403 | funçã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?Tnã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.Mapnão muda o tipo (T→T).Fenner.Resultmora emCompiler\extras, fora da RTL: o.configprecisa deConfig All SourcePathapontando para lá, como o do exemplo acima.- Um
TResultescrito comochoicetambém é possível (Tipos soma), mas oFenner.Resulté o oficial.
Veja também#
- Nulos e exceções, para o outro estilo de tratar erros.
- Demo
44-result-propaga; manualtry-propagate.md.