Skip to content

Instantly share code, notes, and snippets.

@Savelenko
Created January 9, 2025 17:35
Show Gist options
  • Save Savelenko/2303e3e3fe8ca8f0ff5c413f88bed113 to your computer and use it in GitHub Desktop.
Save Savelenko/2303e3e3fe8ca8f0ff5c413f88bed113 to your computer and use it in GitHub Desktop.
F# Result error merging with IWSAMs
module ErrorMerging
open FsToolkit.ErrorHandling
(* Regular approach with two distinct error types *)
type SensorReadings = int
type EngineError =
| Overheated
| LowOil
let startEngine0 (sensorReading : SensorReadings) : Result<unit, EngineError> =
if sensorReading <= 10 then Error Overheated
elif sensorReading <= 20 then Error LowOil
else Ok ()
type AudioError =
| DeviceNotPaired
let initializeAudio0 (sensorReading : SensorReadings) : Result<unit, AudioError> =
if sensorReading <= 30 then Error DeviceNotPaired else Ok ()
// We must unify the engine and audio errors in one DU as follows:
type CarError =
| EngineError of EngineError
| AudioError of AudioError
// ... in order to use sub-computations which return different error types in on expression.
let startCar0 (sensorReading : SensorReadings) : Result<unit, CarError> = result {
// Prioritize engine errors above audio errors
do! startEngine0 sensorReading |> Result.mapError EngineError
do! initializeAudio0 sensorReading |> Result.mapError AudioError
}
(* Automatic merging of errors using IWSAMs *)
// Represent errors not as DUs but as "abstract languages" using IWSAMs
type EngineError<'e> =
static abstract Overheated : 'e
static abstract LowOil : 'e
let startEngine<'e when EngineError<'e>> (sensorReading : SensorReadings) : Result<unit, 'e> =
if sensorReading <= 10 then Error 'e.Overheated
elif sensorReading <= 20 then Error 'e.LowOil
else Ok ()
type AudioError<'e> =
static abstract DeviceNotPaired : 'e
let initializeAudio<'e when AudioError<'e>> (sensorReading : SensorReadings) : Result<unit, 'e> =
if sensorReading <= 30 then Error 'e.DeviceNotPaired else Ok ()
// Define a single error type "at the edge" which serves as the interpretation of both error "languages"
type CarError2 =
| Overheated2
| LowOil2
| DeviceNotPaired2
interface EngineError<CarError2> with
static member Overheated = Overheated2
static member LowOil = LowOil2
// There seems to be a compiler bug here. An interesting error is produced very late during
// building if "2" is removed from DU constructor names. I expect it to be possible to
// reuse the names, as normally possible in F#. I filed the bug.
interface AudioError<CarError2> with
static member DeviceNotPaired = DeviceNotPaired2
// Errors automatically merged into the single type by type inference/annotation.
let startCar (sensorReading : SensorReadings) : Result<unit, CarError2> = result {
// Prioritize engine errors above audio errors
do! startEngine sensorReading
do! initializeAudio sensorReading
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment