Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ValueOption support #131

Merged
merged 7 commits into from
Nov 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion paket.dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ source https://api.nuget.org/v3/index.json


storage: none
nuget FSharp.Core 4.6.2
nuget FSharp.Core 4.7.0
nuget Hopac
nuget Microsoft.SourceLink.GitHub prerelease copy_local: true
nuget Ply

group Test
source https://api.nuget.org/v3/index.json
storage: none
nuget FSharp.Core 4.7.0
nuget Expecto
nuget Expecto.Hopac
nuget Fable.Mocha 2.10.0
Expand Down
127 changes: 31 additions & 96 deletions paket.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/FsToolkit.ErrorHandling/FsToolkit.ErrorHandling.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
<Compile Include="ValidationCE.fs" />
<Compile Include="Option.fs" />
<Compile Include="OptionCE.fs" />
<Compile Include="ValueOption.fs" />
<Compile Include="ValueOptionCE.fs" />
<Compile Include="AsyncOption.fs" />
<Compile Include="AsyncOptionCE.fs" />
<Compile Include="AsyncOptionOp.fs" />
Expand Down
13 changes: 12 additions & 1 deletion src/FsToolkit.ErrorHandling/Option.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ namespace FsToolkit.ErrorHandling
[<RequireQualifiedAccess>]
module Option =

let ofValueOption (vopt: _ voption) =
match vopt with
| ValueSome v -> Some v
| ValueNone -> None

let toValueOption (opt: _ option) =
match opt with
| Some v -> ValueSome v
| None -> ValueNone

let traverseResult f opt =
match opt with
| None -> Ok None
Expand Down Expand Up @@ -30,11 +40,12 @@ module Option =
/// <param name="option1">The input option</param>
/// <param name="option2">The input option</param>
/// <returns></returns>
let zip (option1: option<'a>) (option2: option<'b>) =
let zip (option1: 'a option) (option2: 'b option) =
match option1, option2 with
| Some v1, Some v2 -> Some(v1, v2)
| _ -> None


let ofResult =
function
| Ok v -> Some v
Expand Down
7 changes: 5 additions & 2 deletions src/FsToolkit.ErrorHandling/OptionCE.fs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,12 @@ module OptionCE =
member inline _.Source(result: _ option) : _ option = result


let option = OptionBuilder()
// /// <summary>
// /// Method lets us transform data types into our internal representation.
// /// </summary>
member inline _.Source(vopt: ValueOption<_>) : Option<_> = vopt |> Option.ofValueOption

let option = OptionBuilder()

[<AutoOpen>]
// Having members as extensions gives them lower priority in
Expand Down Expand Up @@ -102,7 +106,6 @@ module OptionExtensions =
/// </summary>
member inline _.Source(s: #seq<_>) = s


// /// <summary>
// /// Method lets us transform data types into our internal representation.
// /// </summary>
Expand Down
53 changes: 53 additions & 0 deletions src/FsToolkit.ErrorHandling/ValueOption.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
namespace FsToolkit.ErrorHandling

#if !FABLE_COMPILER
[<RequireQualifiedAccess>]
module ValueOption =

let ofOption (opt: 'a option) =
match opt with
| Some v -> ValueSome v
| None -> ValueNone

let toOption (vopt: 'a voption) =
match vopt with
| ValueSome v -> Some v
| ValueNone -> None

let traverseResult f vopt =
match vopt with
| ValueNone -> Ok ValueNone
| ValueSome v -> f v |> Result.map ValueSome

let sequenceResult opt = traverseResult id opt

let inline tryParse< ^T when ^T: (static member TryParse : string * byref< ^T > -> bool) and ^T: (new : unit -> ^T)>
valueToParse
=
let mutable output = new ^T()

let parsed =
(^T: (static member TryParse : string * byref< ^T > -> bool) (valueToParse, &output))

match parsed with
| true -> ValueSome output
| _ -> ValueNone

/// <summary>
/// Takes two voptions and returns a tuple of the pair or none if either are none
/// </summary>
/// <param name="voption1">The input option</param>
/// <param name="voption2">The input option</param>
/// <returns></returns>
let zip (voption1: 'a voption) (voption2: 'b voption) =
match voption1, voption2 with
| ValueSome v1, ValueSome v2 -> ValueSome(v1, v2)
| _ -> ValueNone


let ofResult =
function
| Ok v -> ValueSome v
| Error _ -> ValueNone

#endif
118 changes: 118 additions & 0 deletions src/FsToolkit.ErrorHandling/ValueOptionCE.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
namespace FsToolkit.ErrorHandling


#if !FABLE_COMPILER
[<AutoOpen>]
module ValueOptionCE =
open System

type ValueOptionBuilder() =
member inline _.Return(x) = ValueSome x

member inline _.ReturnFrom(m: 'T voption) = m

member inline _.Bind(m: 'a voption, f: 'a -> 'b voption) = ValueOption.bind f m

// Could not get it to work solely with Source. In loop cases it would potentially match the #seq overload and ask for type annotation
member this.Bind(m: 'a when 'a: null, f: 'a -> 'b voption) = this.Bind(m |> ValueOption.ofObj, f)

member inline this.Zero() = this.Return()

member inline _.Combine(m, f) = ValueOption.bind f m
member inline this.Combine(m1: _ voption, m2: _ voption) = this.Bind(m1, (fun () -> m2))

member inline _.Delay(f: unit -> _) = f

member inline _.Run(f) = f ()

member inline this.TryWith(m, h) =
try
this.Run m
with
| e -> h e

member inline this.TryFinally(m, compensation) =
try
this.Run m
finally
compensation ()

member inline this.Using(resource: 'T :> IDisposable, binder) : _ voption =
this.TryFinally(
(fun () -> binder resource),
(fun () ->
if not <| obj.ReferenceEquals(resource, null) then
resource.Dispose())
)

member this.While(guard: unit -> bool, generator: unit -> _ voption) : _ voption =
if not <| guard () then
this.Zero()
else
this.Bind(this.Run generator, (fun () -> this.While(guard, generator)))

member inline this.For(sequence: #seq<'T>, binder: 'T -> _ voption) : _ voption =
this.Using(
sequence.GetEnumerator(),
fun enum -> this.While(enum.MoveNext, this.Delay(fun () -> binder enum.Current))
)

member inline _.BindReturn(x, f) = ValueOption.map f x

member inline _.BindReturn(x, f) =
x |> ValueOption.ofObj |> ValueOption.map f

member inline _.MergeSources(option1, option2) = ValueOption.zip option1 option2

/// <summary>
/// Method lets us transform data types into our internal representation. This is the identity method to recognize the self type.
///
/// See https://stackoverflow.com/questions/35286541/why-would-you-use-builder-source-in-a-custom-computation-expression-builder
/// </summary>
member inline _.Source(result: _ voption) : _ voption = result


// /// <summary>
// /// Method lets us transform data types into our internal representation.
// /// </summary>
member inline _.Source(vopt: _ option) : _ voption = vopt |> ValueOption.ofOption

let voption = ValueOptionBuilder()

[<AutoOpen>]
// Having members as extensions gives them lower priority in
// overload resolution and allows skipping more type annotations.
module ValueOptionExtensionsLower =
type ValueOptionBuilder with
member inline _.Source(nullableObj: 'a when 'a: null) = nullableObj |> ValueOption.ofObj
member inline _.Source(m: string) = m |> ValueOption.ofObj

member inline _.MergeSources(nullableObj1, option2) =
ValueOption.zip (ValueOption.ofObj nullableObj1) option2


member inline _.MergeSources(option1, nullableObj2) =
ValueOption.zip (option1) (ValueOption.ofObj nullableObj2)


member inline _.MergeSources(nullableObj1, nullableObj2) =
ValueOption.zip (ValueOption.ofObj nullableObj1) (ValueOption.ofObj nullableObj2)

[<AutoOpen>]
// Having members as extensions gives them lower priority in
// overload resolution and allows skipping more type annotations.
// The later declared, the higher than previous extension methods, this is magic
module ValueOptionExtensions =
open System

type ValueOptionBuilder with
/// <summary>
/// Needed to allow `for..in` and `for..do` functionality
/// </summary>
member inline _.Source(s: #seq<_>) = s

// /// <summary>
// /// Method lets us transform data types into our internal representation.
// /// </summary>
member inline _.Source(nullable: Nullable<'a>) : 'a voption = nullable |> ValueOption.ofNullable
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
<Compile Include="ResultOption.fs" />
<Compile Include="Option.fs" />
<Compile Include="OptionCE.fs" />
<Compile Include="ValueOption.fs" />
<Compile Include="ValueOptionCE.fs" />
<Compile Include="AsyncOption.fs" />
<Compile Include="AsyncOptionCE.fs" />
<Compile Include="List.fs" />
Expand Down
4 changes: 4 additions & 0 deletions tests/FsToolkit.ErrorHandling.Tests/Main.fs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ let allTests =
ResultOptionTests.allTests
OptionTests.allTests
OptionCETests.allTests
#if !FABLE_COMPILER
ValueOptionTests.allTests
ValueOptionCETests.allTests
#endif
AsyncOptionTests.allTests
AsyncOptionCETests.allTests
ListTests.allTests
Expand Down
26 changes: 24 additions & 2 deletions tests/FsToolkit.ErrorHandling.Tests/OptionCE.fs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ let ceTests =
try
return data
with
| e -> return! raise e
| e -> return raise e
}

Expect.equal actual (Some data) "Try with failed"
Expand Down Expand Up @@ -442,7 +442,29 @@ let ``OptionCE applicative tests`` =
return a + b - c
}

Expect.equal actual None "Should be None" ]
Expect.equal actual None "Should be None"

testCase "ValueOption.Some"
<| fun () ->
let actual =
option {
let! a = ValueSome 3
return a
}

Expect.equal actual (Some 3) "Should be None"

testCase "ValueOption.None"
<| fun () ->
let actual =
option {
let! a = ValueNone
return a
}

Expect.equal actual (None) "Should be None" ]



let allTests =
testList
Expand Down
77 changes: 77 additions & 0 deletions tests/FsToolkit.ErrorHandling.Tests/ValueOption.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
module ValueOptionTests

#if !FABLE_COMPILER
open System
open Expecto
open SampleDomain
open TestData
open TestHelpers
open FsToolkit.ErrorHandling


let traverseResultTests =
testList
"ValueOption.traverseResult Tests"
[ testCase "traverseResult with ValueSome of valid data"
<| fun _ ->
let (latitude, longitude) = (ValueSome lat), (ValueSome lng)

latitude
|> ValueOption.traverseResult Latitude.TryCreate
|> Expect.hasOkValue (ValueSome validLat)

longitude
|> ValueOption.traverseResult Longitude.TryCreate
|> Expect.hasOkValue (ValueSome validLng) ]


let tryParseTests =
testList
"ValueOption.tryParse"
[ testCase "Can Parse int"
<| fun _ ->
let expected = 3

let actual =
ValueOption.tryParse<int> (string expected)

Expect.equal actual (ValueSome expected) "Should be parsed"

testCase "Can Parse double"
<| fun _ ->
let expected: float = 3.0

let actual =
ValueOption.tryParse<float> (string expected)

Expect.equal actual (ValueSome expected) "Should be parsed"

testCase "Can Parse Guid"
<| fun _ ->
let expectedGuid = Guid.NewGuid()

let parsedValue =
ValueOption.tryParse<Guid> (string expectedGuid)

Expect.equal parsedValue (ValueSome expectedGuid) "Should be same guid"

]


let ofResultTests =
testList
"ValueOption.ofResult Tests"
[ testCase "ofResult simple cases"
<| fun _ ->
Expect.equal (ValueOption.ofResult (Ok 123)) (ValueSome 123) "Ok int"
Expect.equal (ValueOption.ofResult (Ok "abc")) (ValueSome "abc") "Ok string"
Expect.equal (ValueOption.ofResult (Error "x")) ValueNone "Error _" ]


let allTests =
testList
"ValueOption Tests"
[ traverseResultTests
tryParseTests
ofResultTests ]
#endif
Loading