Created
January 9, 2025 17:35
-
-
Save Savelenko/2303e3e3fe8ca8f0ff5c413f88bed113 to your computer and use it in GitHub Desktop.
F# Result error merging with IWSAMs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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