Elegant Solution for Discriminated Unions Representing Failures in F#What is the most idiomatic way of representing errors in F#How to use symbols/punctuation characters in discriminated unionsType-safe discriminated unions in C#, or: How to limit the number of implementations of an interface?How to enumerate a discriminated union in F#?F# understanding discriminated unionf#: constant union case tag numberF# Subtype Discriminated UnionsDocumenting discriminated unions in F#F# - Create a Recursive Discriminated Union at RuntimeF# Discriminated Unions and Printing“Merging” Discriminated Unions in F#?
How to realistically deal with a shield user?
Changing Row Keys into Normal Rows
The Game of the Century - why didn't Byrne take the rook after he forked Fischer?
Purchased new computer from DELL with pre-installed Ubuntu. Won't boot. Should assume its an error from DELL?
The meaning of "scale" in "because diversions scale so easily wealth becomes concentrated"
What does the ISO setting for mechanical 35mm film cameras actually do?
Getting an entry level IT position later in life
Can a Hogwarts student refuse the Sorting Hat's decision?
Does a 4 bladed prop have almost twice the thrust of a 2 bladed prop?
What is an air conditioner compressor hard start kit and how does it work?
What could prevent players from leaving an island?
Did Apollo leave poop on the moon?
Is an "are" omitted in this sentence
Ubuntu show wrong disk sizes, how to solve it?
How easy is it to get a gun illegally in the United States?
How many years before enough atoms of your body are replaced to survive the sudden disappearance of the original body’s atoms?
What prevents ads from reading my password as I type it?
Why is the Vasa Museum in Stockholm so Popular?
Is there a way to say "double + any number" in German?
Is it double speak?
I am considering a visit to a Nevada brothel. What should I say at the US border?
Should I take out a personal loan to pay off credit card debt?
What was the role of Commodore-West Germany?
Why should I "believe in" weak solutions to PDEs?
Elegant Solution for Discriminated Unions Representing Failures in F#
What is the most idiomatic way of representing errors in F#How to use symbols/punctuation characters in discriminated unionsType-safe discriminated unions in C#, or: How to limit the number of implementations of an interface?How to enumerate a discriminated union in F#?F# understanding discriminated unionf#: constant union case tag numberF# Subtype Discriminated UnionsDocumenting discriminated unions in F#F# - Create a Recursive Discriminated Union at RuntimeF# Discriminated Unions and Printing“Merging” Discriminated Unions in F#?
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty margin-bottom:0;
I’m moving away from creating and catching exceptions in F# to something built around Result<'T, 'TError>
. I found this, which agrees with my initial pursuit of representing failures with a discriminated union, but I ran into the problem of having a lot of different cases for my Failure
discriminated union:
type TypedValue =
| Integer of int
| Long of int64
| …
type Failure =
| ArgumentOutOfRange of Argument : TypedValue; Minimum : TypedValue; Maximum : TypedValue
| BufferTooSmall of RequiredSize : int
| Exception of exn
| IndexOutOfRange of Index : int
| …
I’d prefer not to have a multitude of types dedicated to error handling. This “typed value” thing is not elegant at all as I either have to create conflicting names (Byte
versus System.Byte
) or create long names to avoid conflict (| UnsignedByte of byte
).
Generics is a possibility, but then what would the 'T
in Failure<'T>
represent? ArgumentOutOfRange
wouldn’t be the only case in the discriminated union, and some cases might require more type parameters or none at all.
error-handling f# conventions
add a comment |
I’m moving away from creating and catching exceptions in F# to something built around Result<'T, 'TError>
. I found this, which agrees with my initial pursuit of representing failures with a discriminated union, but I ran into the problem of having a lot of different cases for my Failure
discriminated union:
type TypedValue =
| Integer of int
| Long of int64
| …
type Failure =
| ArgumentOutOfRange of Argument : TypedValue; Minimum : TypedValue; Maximum : TypedValue
| BufferTooSmall of RequiredSize : int
| Exception of exn
| IndexOutOfRange of Index : int
| …
I’d prefer not to have a multitude of types dedicated to error handling. This “typed value” thing is not elegant at all as I either have to create conflicting names (Byte
versus System.Byte
) or create long names to avoid conflict (| UnsignedByte of byte
).
Generics is a possibility, but then what would the 'T
in Failure<'T>
represent? ArgumentOutOfRange
wouldn’t be the only case in the discriminated union, and some cases might require more type parameters or none at all.
error-handling f# conventions
1
Failure<'T> is normally just a string - an error message. Stop building that Failure DU to cover everything. If you absolutely do need something more than a string, then as a general rule place the failure type(s) needed for that in the domain where they belong, along with the specific success type(s) if there are any.
– Bent Tranberg
Mar 27 at 6:22
And if you haven't discovered this yet, do read it carefully: fsharpforfunandprofit.com/posts/recipe-part2
– Bent Tranberg
Mar 27 at 6:23
Also note that the Result type is a special case of the Choice type, which gives you more than two possible outcomes. Not saying you should start to use that for error handling in a lot of places, but just be aware of its usefulness.
– Bent Tranberg
Mar 27 at 6:34
Exceptions has a common base class, it would be quite difficult in the languages we usually use to have to declare a union exception class of every type. I think one can make a similar argument that theFailure
type should only have a few relevant cases where one case is common base class. The Message ie string also makes sense to me (which I for error handling see as a limit common "base" class).
– Just another metaprogrammer
Mar 27 at 6:56
add a comment |
I’m moving away from creating and catching exceptions in F# to something built around Result<'T, 'TError>
. I found this, which agrees with my initial pursuit of representing failures with a discriminated union, but I ran into the problem of having a lot of different cases for my Failure
discriminated union:
type TypedValue =
| Integer of int
| Long of int64
| …
type Failure =
| ArgumentOutOfRange of Argument : TypedValue; Minimum : TypedValue; Maximum : TypedValue
| BufferTooSmall of RequiredSize : int
| Exception of exn
| IndexOutOfRange of Index : int
| …
I’d prefer not to have a multitude of types dedicated to error handling. This “typed value” thing is not elegant at all as I either have to create conflicting names (Byte
versus System.Byte
) or create long names to avoid conflict (| UnsignedByte of byte
).
Generics is a possibility, but then what would the 'T
in Failure<'T>
represent? ArgumentOutOfRange
wouldn’t be the only case in the discriminated union, and some cases might require more type parameters or none at all.
error-handling f# conventions
I’m moving away from creating and catching exceptions in F# to something built around Result<'T, 'TError>
. I found this, which agrees with my initial pursuit of representing failures with a discriminated union, but I ran into the problem of having a lot of different cases for my Failure
discriminated union:
type TypedValue =
| Integer of int
| Long of int64
| …
type Failure =
| ArgumentOutOfRange of Argument : TypedValue; Minimum : TypedValue; Maximum : TypedValue
| BufferTooSmall of RequiredSize : int
| Exception of exn
| IndexOutOfRange of Index : int
| …
I’d prefer not to have a multitude of types dedicated to error handling. This “typed value” thing is not elegant at all as I either have to create conflicting names (Byte
versus System.Byte
) or create long names to avoid conflict (| UnsignedByte of byte
).
Generics is a possibility, but then what would the 'T
in Failure<'T>
represent? ArgumentOutOfRange
wouldn’t be the only case in the discriminated union, and some cases might require more type parameters or none at all.
error-handling f# conventions
error-handling f# conventions
asked Mar 27 at 2:52
Kevin LiKevin Li
1841 silver badge10 bronze badges
1841 silver badge10 bronze badges
1
Failure<'T> is normally just a string - an error message. Stop building that Failure DU to cover everything. If you absolutely do need something more than a string, then as a general rule place the failure type(s) needed for that in the domain where they belong, along with the specific success type(s) if there are any.
– Bent Tranberg
Mar 27 at 6:22
And if you haven't discovered this yet, do read it carefully: fsharpforfunandprofit.com/posts/recipe-part2
– Bent Tranberg
Mar 27 at 6:23
Also note that the Result type is a special case of the Choice type, which gives you more than two possible outcomes. Not saying you should start to use that for error handling in a lot of places, but just be aware of its usefulness.
– Bent Tranberg
Mar 27 at 6:34
Exceptions has a common base class, it would be quite difficult in the languages we usually use to have to declare a union exception class of every type. I think one can make a similar argument that theFailure
type should only have a few relevant cases where one case is common base class. The Message ie string also makes sense to me (which I for error handling see as a limit common "base" class).
– Just another metaprogrammer
Mar 27 at 6:56
add a comment |
1
Failure<'T> is normally just a string - an error message. Stop building that Failure DU to cover everything. If you absolutely do need something more than a string, then as a general rule place the failure type(s) needed for that in the domain where they belong, along with the specific success type(s) if there are any.
– Bent Tranberg
Mar 27 at 6:22
And if you haven't discovered this yet, do read it carefully: fsharpforfunandprofit.com/posts/recipe-part2
– Bent Tranberg
Mar 27 at 6:23
Also note that the Result type is a special case of the Choice type, which gives you more than two possible outcomes. Not saying you should start to use that for error handling in a lot of places, but just be aware of its usefulness.
– Bent Tranberg
Mar 27 at 6:34
Exceptions has a common base class, it would be quite difficult in the languages we usually use to have to declare a union exception class of every type. I think one can make a similar argument that theFailure
type should only have a few relevant cases where one case is common base class. The Message ie string also makes sense to me (which I for error handling see as a limit common "base" class).
– Just another metaprogrammer
Mar 27 at 6:56
1
1
Failure<'T> is normally just a string - an error message. Stop building that Failure DU to cover everything. If you absolutely do need something more than a string, then as a general rule place the failure type(s) needed for that in the domain where they belong, along with the specific success type(s) if there are any.
– Bent Tranberg
Mar 27 at 6:22
Failure<'T> is normally just a string - an error message. Stop building that Failure DU to cover everything. If you absolutely do need something more than a string, then as a general rule place the failure type(s) needed for that in the domain where they belong, along with the specific success type(s) if there are any.
– Bent Tranberg
Mar 27 at 6:22
And if you haven't discovered this yet, do read it carefully: fsharpforfunandprofit.com/posts/recipe-part2
– Bent Tranberg
Mar 27 at 6:23
And if you haven't discovered this yet, do read it carefully: fsharpforfunandprofit.com/posts/recipe-part2
– Bent Tranberg
Mar 27 at 6:23
Also note that the Result type is a special case of the Choice type, which gives you more than two possible outcomes. Not saying you should start to use that for error handling in a lot of places, but just be aware of its usefulness.
– Bent Tranberg
Mar 27 at 6:34
Also note that the Result type is a special case of the Choice type, which gives you more than two possible outcomes. Not saying you should start to use that for error handling in a lot of places, but just be aware of its usefulness.
– Bent Tranberg
Mar 27 at 6:34
Exceptions has a common base class, it would be quite difficult in the languages we usually use to have to declare a union exception class of every type. I think one can make a similar argument that the
Failure
type should only have a few relevant cases where one case is common base class. The Message ie string also makes sense to me (which I for error handling see as a limit common "base" class).– Just another metaprogrammer
Mar 27 at 6:56
Exceptions has a common base class, it would be quite difficult in the languages we usually use to have to declare a union exception class of every type. I think one can make a similar argument that the
Failure
type should only have a few relevant cases where one case is common base class. The Message ie string also makes sense to me (which I for error handling see as a limit common "base" class).– Just another metaprogrammer
Mar 27 at 6:56
add a comment |
2 Answers
2
active
oldest
votes
Using Result<'T, 'TError>
makes a lot of sense in cases where you have custom kinds of errors that you definitely need to handle or in cases where you have some other logic for propagating errors than the one implemented by standard exceptions (e.g. if you can continue running code despite the fact that there was an error). However, I would not use it as a 1:1 replacement for exceptions - it will just make your code unnecessarilly complicated and cumbersome without really giving you much benefits.
To answer your question, since you are mirroring standard .NET exceptions in your discriminated union, you could probably just use a standard .NET exception in your Result
type and use Result<'T, exn>
as your data type:
if arg < 10 then Error(ArgumentOutOfRangeException("arg", "Value is too small"))
else OK(arg - 1)
Regarding the ArgumentOutOfRange
union case and TypedValue
- the reason for using something like TypedValue
is typically that you need to pattern match on the possible values and do something with them. In case of exceptions, what do you want to do with the values? If you just need to report them to the user, then you can use obj
which will let you easily print them (it won't be that easy to get the numerical values and do some further calculations with them, but I don't think you need that).
type Failure =
| ArgumentOutOfRange of
add a comment |
Another option (and what I normally do, personally) is to model your domain-specific failures with specific cases in your Failure
union, and then have a general-purpose UnexpectedError
case that takes an exn
as its data and handles any non-domain-related failures. Then, when an error from one domain occurs in another, you can use Result.mapError
to convert between them. Here's an example from a real domain I've modeled:
open System
// Top-level domain failures
type EntityValidationError =
| EntityIdMustBeGreaterThanZero of int64
| InvalidTenant of string
| UnexpectedException of exn
// Sub-domain specific failures
type AccountValidationError =
| AccountNumberMustBeTenDigits of string
| AccountNameIsRequired of string
| EntityValidationError of EntityValidationError // Sub-domain representaiton of top-level failures
| AccountValidationUnexpectedException of exn
// Sub-domain Entity
// The fields would probably be single-case unions rather than primitives
type Account =
Id: int64
AccountNumber: string
module EntityId =
let validate id =
if id > 0L
then Ok id
else Error (EntityIdMustBeGreaterThanZero id)
module AccountNumber =
let validate number =
if number |> String.length = 10 && number |> Seq.forall Char.IsDigit
then Ok number
else Error (AccountNumberMustBeTenDigits number)
module Account =
let create id number =
id
|> EntityId.validate
|> Result.mapError EntityValidationError // Convert to sub-domain error type
|> Result.bind (fun entityId ->
number
|> AccountNumber.validate
|> Result.map (fun accountNumber -> Id = entityId; AccountNumber = accountNumber ))
add a comment |
Your Answer
StackExchange.ifUsing("editor", function ()
StackExchange.using("externalEditor", function ()
StackExchange.using("snippets", function ()
StackExchange.snippets.init();
);
);
, "code-snippets");
StackExchange.ready(function()
var channelOptions =
tags: "".split(" "),
id: "1"
;
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function()
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled)
StackExchange.using("snippets", function()
createEditor();
);
else
createEditor();
);
function createEditor()
StackExchange.prepareEditor(
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader:
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
,
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
);
);
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f55369082%2felegant-solution-for-discriminated-unions-representing-failures-in-f%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
2 Answers
2
active
oldest
votes
2 Answers
2
active
oldest
votes
active
oldest
votes
active
oldest
votes
Using Result<'T, 'TError>
makes a lot of sense in cases where you have custom kinds of errors that you definitely need to handle or in cases where you have some other logic for propagating errors than the one implemented by standard exceptions (e.g. if you can continue running code despite the fact that there was an error). However, I would not use it as a 1:1 replacement for exceptions - it will just make your code unnecessarilly complicated and cumbersome without really giving you much benefits.
To answer your question, since you are mirroring standard .NET exceptions in your discriminated union, you could probably just use a standard .NET exception in your Result
type and use Result<'T, exn>
as your data type:
if arg < 10 then Error(ArgumentOutOfRangeException("arg", "Value is too small"))
else OK(arg - 1)
Regarding the ArgumentOutOfRange
union case and TypedValue
- the reason for using something like TypedValue
is typically that you need to pattern match on the possible values and do something with them. In case of exceptions, what do you want to do with the values? If you just need to report them to the user, then you can use obj
which will let you easily print them (it won't be that easy to get the numerical values and do some further calculations with them, but I don't think you need that).
type Failure =
| ArgumentOutOfRange of
add a comment |
Using Result<'T, 'TError>
makes a lot of sense in cases where you have custom kinds of errors that you definitely need to handle or in cases where you have some other logic for propagating errors than the one implemented by standard exceptions (e.g. if you can continue running code despite the fact that there was an error). However, I would not use it as a 1:1 replacement for exceptions - it will just make your code unnecessarilly complicated and cumbersome without really giving you much benefits.
To answer your question, since you are mirroring standard .NET exceptions in your discriminated union, you could probably just use a standard .NET exception in your Result
type and use Result<'T, exn>
as your data type:
if arg < 10 then Error(ArgumentOutOfRangeException("arg", "Value is too small"))
else OK(arg - 1)
Regarding the ArgumentOutOfRange
union case and TypedValue
- the reason for using something like TypedValue
is typically that you need to pattern match on the possible values and do something with them. In case of exceptions, what do you want to do with the values? If you just need to report them to the user, then you can use obj
which will let you easily print them (it won't be that easy to get the numerical values and do some further calculations with them, but I don't think you need that).
type Failure =
| ArgumentOutOfRange of
add a comment |
Using Result<'T, 'TError>
makes a lot of sense in cases where you have custom kinds of errors that you definitely need to handle or in cases where you have some other logic for propagating errors than the one implemented by standard exceptions (e.g. if you can continue running code despite the fact that there was an error). However, I would not use it as a 1:1 replacement for exceptions - it will just make your code unnecessarilly complicated and cumbersome without really giving you much benefits.
To answer your question, since you are mirroring standard .NET exceptions in your discriminated union, you could probably just use a standard .NET exception in your Result
type and use Result<'T, exn>
as your data type:
if arg < 10 then Error(ArgumentOutOfRangeException("arg", "Value is too small"))
else OK(arg - 1)
Regarding the ArgumentOutOfRange
union case and TypedValue
- the reason for using something like TypedValue
is typically that you need to pattern match on the possible values and do something with them. In case of exceptions, what do you want to do with the values? If you just need to report them to the user, then you can use obj
which will let you easily print them (it won't be that easy to get the numerical values and do some further calculations with them, but I don't think you need that).
type Failure =
| ArgumentOutOfRange of
Using Result<'T, 'TError>
makes a lot of sense in cases where you have custom kinds of errors that you definitely need to handle or in cases where you have some other logic for propagating errors than the one implemented by standard exceptions (e.g. if you can continue running code despite the fact that there was an error). However, I would not use it as a 1:1 replacement for exceptions - it will just make your code unnecessarilly complicated and cumbersome without really giving you much benefits.
To answer your question, since you are mirroring standard .NET exceptions in your discriminated union, you could probably just use a standard .NET exception in your Result
type and use Result<'T, exn>
as your data type:
if arg < 10 then Error(ArgumentOutOfRangeException("arg", "Value is too small"))
else OK(arg - 1)
Regarding the ArgumentOutOfRange
union case and TypedValue
- the reason for using something like TypedValue
is typically that you need to pattern match on the possible values and do something with them. In case of exceptions, what do you want to do with the values? If you just need to report them to the user, then you can use obj
which will let you easily print them (it won't be that easy to get the numerical values and do some further calculations with them, but I don't think you need that).
type Failure =
| ArgumentOutOfRange of
answered Mar 27 at 11:59
Tomas PetricekTomas Petricek
206k15 gold badges303 silver badges477 bronze badges
206k15 gold badges303 silver badges477 bronze badges
add a comment |
add a comment |
Another option (and what I normally do, personally) is to model your domain-specific failures with specific cases in your Failure
union, and then have a general-purpose UnexpectedError
case that takes an exn
as its data and handles any non-domain-related failures. Then, when an error from one domain occurs in another, you can use Result.mapError
to convert between them. Here's an example from a real domain I've modeled:
open System
// Top-level domain failures
type EntityValidationError =
| EntityIdMustBeGreaterThanZero of int64
| InvalidTenant of string
| UnexpectedException of exn
// Sub-domain specific failures
type AccountValidationError =
| AccountNumberMustBeTenDigits of string
| AccountNameIsRequired of string
| EntityValidationError of EntityValidationError // Sub-domain representaiton of top-level failures
| AccountValidationUnexpectedException of exn
// Sub-domain Entity
// The fields would probably be single-case unions rather than primitives
type Account =
Id: int64
AccountNumber: string
module EntityId =
let validate id =
if id > 0L
then Ok id
else Error (EntityIdMustBeGreaterThanZero id)
module AccountNumber =
let validate number =
if number |> String.length = 10 && number |> Seq.forall Char.IsDigit
then Ok number
else Error (AccountNumberMustBeTenDigits number)
module Account =
let create id number =
id
|> EntityId.validate
|> Result.mapError EntityValidationError // Convert to sub-domain error type
|> Result.bind (fun entityId ->
number
|> AccountNumber.validate
|> Result.map (fun accountNumber -> Id = entityId; AccountNumber = accountNumber ))
add a comment |
Another option (and what I normally do, personally) is to model your domain-specific failures with specific cases in your Failure
union, and then have a general-purpose UnexpectedError
case that takes an exn
as its data and handles any non-domain-related failures. Then, when an error from one domain occurs in another, you can use Result.mapError
to convert between them. Here's an example from a real domain I've modeled:
open System
// Top-level domain failures
type EntityValidationError =
| EntityIdMustBeGreaterThanZero of int64
| InvalidTenant of string
| UnexpectedException of exn
// Sub-domain specific failures
type AccountValidationError =
| AccountNumberMustBeTenDigits of string
| AccountNameIsRequired of string
| EntityValidationError of EntityValidationError // Sub-domain representaiton of top-level failures
| AccountValidationUnexpectedException of exn
// Sub-domain Entity
// The fields would probably be single-case unions rather than primitives
type Account =
Id: int64
AccountNumber: string
module EntityId =
let validate id =
if id > 0L
then Ok id
else Error (EntityIdMustBeGreaterThanZero id)
module AccountNumber =
let validate number =
if number |> String.length = 10 && number |> Seq.forall Char.IsDigit
then Ok number
else Error (AccountNumberMustBeTenDigits number)
module Account =
let create id number =
id
|> EntityId.validate
|> Result.mapError EntityValidationError // Convert to sub-domain error type
|> Result.bind (fun entityId ->
number
|> AccountNumber.validate
|> Result.map (fun accountNumber -> Id = entityId; AccountNumber = accountNumber ))
add a comment |
Another option (and what I normally do, personally) is to model your domain-specific failures with specific cases in your Failure
union, and then have a general-purpose UnexpectedError
case that takes an exn
as its data and handles any non-domain-related failures. Then, when an error from one domain occurs in another, you can use Result.mapError
to convert between them. Here's an example from a real domain I've modeled:
open System
// Top-level domain failures
type EntityValidationError =
| EntityIdMustBeGreaterThanZero of int64
| InvalidTenant of string
| UnexpectedException of exn
// Sub-domain specific failures
type AccountValidationError =
| AccountNumberMustBeTenDigits of string
| AccountNameIsRequired of string
| EntityValidationError of EntityValidationError // Sub-domain representaiton of top-level failures
| AccountValidationUnexpectedException of exn
// Sub-domain Entity
// The fields would probably be single-case unions rather than primitives
type Account =
Id: int64
AccountNumber: string
module EntityId =
let validate id =
if id > 0L
then Ok id
else Error (EntityIdMustBeGreaterThanZero id)
module AccountNumber =
let validate number =
if number |> String.length = 10 && number |> Seq.forall Char.IsDigit
then Ok number
else Error (AccountNumberMustBeTenDigits number)
module Account =
let create id number =
id
|> EntityId.validate
|> Result.mapError EntityValidationError // Convert to sub-domain error type
|> Result.bind (fun entityId ->
number
|> AccountNumber.validate
|> Result.map (fun accountNumber -> Id = entityId; AccountNumber = accountNumber ))
Another option (and what I normally do, personally) is to model your domain-specific failures with specific cases in your Failure
union, and then have a general-purpose UnexpectedError
case that takes an exn
as its data and handles any non-domain-related failures. Then, when an error from one domain occurs in another, you can use Result.mapError
to convert between them. Here's an example from a real domain I've modeled:
open System
// Top-level domain failures
type EntityValidationError =
| EntityIdMustBeGreaterThanZero of int64
| InvalidTenant of string
| UnexpectedException of exn
// Sub-domain specific failures
type AccountValidationError =
| AccountNumberMustBeTenDigits of string
| AccountNameIsRequired of string
| EntityValidationError of EntityValidationError // Sub-domain representaiton of top-level failures
| AccountValidationUnexpectedException of exn
// Sub-domain Entity
// The fields would probably be single-case unions rather than primitives
type Account =
Id: int64
AccountNumber: string
module EntityId =
let validate id =
if id > 0L
then Ok id
else Error (EntityIdMustBeGreaterThanZero id)
module AccountNumber =
let validate number =
if number |> String.length = 10 && number |> Seq.forall Char.IsDigit
then Ok number
else Error (AccountNumberMustBeTenDigits number)
module Account =
let create id number =
id
|> EntityId.validate
|> Result.mapError EntityValidationError // Convert to sub-domain error type
|> Result.bind (fun entityId ->
number
|> AccountNumber.validate
|> Result.map (fun accountNumber -> Id = entityId; AccountNumber = accountNumber ))
answered Mar 27 at 12:50
Aaron M. EshbachAaron M. Eshbach
5,3559 silver badges19 bronze badges
5,3559 silver badges19 bronze badges
add a comment |
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f55369082%2felegant-solution-for-discriminated-unions-representing-failures-in-f%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
1
Failure<'T> is normally just a string - an error message. Stop building that Failure DU to cover everything. If you absolutely do need something more than a string, then as a general rule place the failure type(s) needed for that in the domain where they belong, along with the specific success type(s) if there are any.
– Bent Tranberg
Mar 27 at 6:22
And if you haven't discovered this yet, do read it carefully: fsharpforfunandprofit.com/posts/recipe-part2
– Bent Tranberg
Mar 27 at 6:23
Also note that the Result type is a special case of the Choice type, which gives you more than two possible outcomes. Not saying you should start to use that for error handling in a lot of places, but just be aware of its usefulness.
– Bent Tranberg
Mar 27 at 6:34
Exceptions has a common base class, it would be quite difficult in the languages we usually use to have to declare a union exception class of every type. I think one can make a similar argument that the
Failure
type should only have a few relevant cases where one case is common base class. The Message ie string also makes sense to me (which I for error handling see as a limit common "base" class).– Just another metaprogrammer
Mar 27 at 6:56