Я работаю над единичным размеченным союзом с модулем для создания экземпляров типа и возврата результата либо «ОК», если ввод был действительным, либо «Ошибка» в противном случае. Вот что у меня есть до сих пор.
type ErrorMessage = string
type NonNegativeInt = private NonNegativeInt of int
module NonNegativeInt =
let create (inputInt:int) : Result<NonNegativeInt, ErrorMessage> =
if inputInt >= 0 then
Ok (NonNegativeInt inputInt)
else
Error ("inputInt must be >= 0")
let value (NonNegativeInt intVal) = intVal
Я хотел бы добавить целое число к экземпляру этого типа, используя функцию создания, чтобы он блокировал отрицательные значения. У меня первый тест работает таким образом.
[<Fact>]
member this.``NonNegativeInt test`` () =
let nonNegativeResult = NonNegativeInt.create 5
let newNonNegativeResult = match nonNegativeResult with
| Ok result ->
let intVal = NonNegativeInt.value result
let newIntVal = intVal + 1
NonNegativeInt.create newIntVal
| Error _ ->
nonNegativeResult
match newNonNegativeResult with
| Ok result ->
Assert.Equal(6, NonNegativeInt.value result)
| Error _ ->
Assert.Fail("Error creating new NonNegativeInt")
Это в значительной степени непригодно для использования таким образом. Есть ли более краткий способ выполнить эту задачу без всей этой распаковки, упаковки и сопоставления с образцом? Подходит ли Result.bind?
Это лучше, но все еще кажется немного неуклюжим. Возможно, модулю NonNegativeInt
нужна еще одна функция, кроме create и value, чтобы упростить эту задачу.
[<Fact>]
member this.``NonNegativeInt test2`` () =
let nni1 = NonNegativeInt.create 5
let nni2 = nni1
|> Result.bind (fun x -> NonNegativeInt.create ((NonNegativeInt.value x) + 1))
let expectedResult = NonNegativeInt.create 6
Assert.Equal(expectedResult, nni2)
Вы можете использовать построитель вычислений, чтобы сделать код чище:
type ResultBuilder() =
member _.Return(x) = Ok x
member _.ReturnFrom(res : Result<_, _>) = res
member _.Bind(res, f) = Result.bind f res
let result = ResultBuilder()
Тогда ваш пример становится:
let test () =
result {
let! nni = NonNegativeInt.create 5
return! NonNegativeInt.create (nni.Value + 1)
}
test () |> printfn "%A" // Ok NonNegativeInt 6
Я также добавил элемент, чтобы упростить доступ к значению NonNegativeInteger:
type NonNegativeInt =
private NonNegativeInt of int
with
member this.Value =
let (NonNegativeInt n) = this in n
Иметь тип NNI, а затем оборачивать его в Result — это все равно, что носить одновременно ремень и подтяжки. Чтобы еще больше упростить ситуацию, вы можете полностью избавиться от типа NNI и просто сохранить логику проверки:
module NonNegativeInt =
let create (inputInt:int) : Result<int, ErrorMessage> =
if inputInt >= 0 then
Ok inputInt
else
Error ("inputInt must be >= 0")
let test () =
result {
let! n = NonNegativeInt.create 5
return! NonNegativeInt.create (n + 1)
}
test () |> printfn "%A" // Ok 6
В качестве альтернативы вы можете сохранить тип NNI и доверить вызывающему объекту его использование с допустимыми значениями (без переноса в результат). Вот что делает FsCheck, например:
///Represents an int >= 0
type NonNegativeInt = NonNegativeInt of int with
member x.Get = match x with NonNegativeInt r -> r
override x.ToString() = x.Get.ToString()
static member op_Explicit(NonNegativeInt i) = i
Вычислительные выражения действительно великолепны. Вы можете использовать один внутри другого, но если вы собираетесь работать с Task<Result<_, _>>
, вероятно, лучше использовать конструктор специально для этого типа. Есть один здесь, но я его не пробовала.
Отлично! Потребуется минута, чтобы понять это. Выражение вычисления удивительно. Мне нужно действительно пристегнуться и преодолеть мой ментальный блок на них. Они, вероятно, не так сложны, как я себе представляю. Можно ли использовать вычисляемое выражение результата внутри какого-либо другого вычислительного выражения, такого как task{}?