Skip to content

Commit c4c3da0

Browse files
authored
Merge pull request #7 from TypedUseCase/feature/parse-domain-async
Parse domain asynchronously
2 parents f9c28e3 + faa7cd4 commit c4c3da0

File tree

5 files changed

+169
-48
lines changed

5 files changed

+169
-48
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
<!-- There is always Unreleased section on the top. Subsections (Add, Changed, Fix, Removed) should be Add as needed. -->
44
## Unreleased
5+
- [**BC**] Change function `Parser.parse` to return Async Result
6+
- [**BC**] Change `Resolver.resolve` function to return a `ResolveError` on error
7+
- Add `Resolver.resolveAsync` function
8+
- [_Internal_] Fix `asyncResult` computation expression
59

610
## 2.1.0 - 2021-08-11
711
- Update dependencies

src/Domain/Parser.fs

+46-15
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,32 @@ open FSharp.Compiler.Text
55

66
type ParsedDomain = ParsedDomain of FSharpCheckProjectResults
77

8+
type ParseError =
9+
| ProjectOptionsNotAvailable of exn
10+
| ParseAndCheckProjectFailed of exn
11+
| ParseError of exn
12+
13+
[<RequireQualifiedAccess>]
14+
module ParseError =
15+
let format = function
16+
| ProjectOptionsNotAvailable e -> sprintf "[Domain Parser][Parse error]: Project options not available due to:\n%A" e
17+
| ParseAndCheckProjectFailed e -> sprintf "[Domain Parser][Parse error]: Project parsing and checking failed on:\n%A" e
18+
| ParseError e -> sprintf "[Domain Parser][Parse error]: Parsing failed on:\n%A" e
19+
820
[<RequireQualifiedAccess>]
921
module Parser =
1022
open System
1123
open System.IO
24+
open ErrorHandling
1225

13-
let private parseAndCheck (output: MF.ConsoleApplication.Output) (checker: FSharpChecker) (file, input) =
26+
let private parseAndCheck (output: MF.ConsoleApplication.Output) (checker: FSharpChecker) (file, input) = asyncResult {
1427
if output.IsVerbose() then output.Title "ParseAndCheck"
1528

1629
if output.IsVeryVerbose() then output.Section "GetProjectOptionsFromScript"
17-
let projOptions, errors =
30+
31+
let! projOptions, errors =
1832
checker.GetProjectOptionsFromScript(file, SourceText.ofString input)
19-
|> Async.RunSynchronously
33+
|> AsyncResult.ofAsyncCatch ProjectOptionsNotAvailable
2034

2135
if output.IsVeryVerbose() then output.Message "Ok"
2236
if output.IsDebug() then output.Message <| sprintf "ProjOptions:\n%A" projOptions
@@ -54,17 +68,24 @@ module Parser =
5468
if output.IsVeryVerbose() then output.Message "Ok"
5569

5670
if output.IsVeryVerbose() then output.Section "ParseAndCheckProject"
57-
checker.ParseAndCheckProject (fprojOptions)
58-
|> Async.RunSynchronously
59-
|> tee (fun parseFileResults ->
60-
if output.IsVeryVerbose() then output.Message "Ok"
61-
62-
if output.IsDebug() then output.Options "Result:" [
63-
["DependencyFiles"; parseFileResults.DependencyFiles |> sprintf "%A"]
64-
["Errors"; parseFileResults.HasCriticalErrors |> sprintf "%A"]
65-
["ProjectContext"; parseFileResults.ProjectContext |> sprintf "%A"]
66-
]
67-
)
71+
72+
let! checkProjectResults =
73+
fprojOptions
74+
|> checker.ParseAndCheckProject
75+
|> AsyncResult.ofAsyncCatch ParseAndCheckProjectFailed
76+
77+
return
78+
checkProjectResults
79+
|> tee (fun parseFileResults ->
80+
if output.IsVeryVerbose() then output.Message "Ok"
81+
82+
if output.IsDebug() then output.Options "Result:" [
83+
["DependencyFiles"; parseFileResults.DependencyFiles |> sprintf "%A"]
84+
["Errors"; parseFileResults.HasCriticalErrors |> sprintf "%A"]
85+
["ProjectContext"; parseFileResults.ProjectContext |> sprintf "%A"]
86+
]
87+
)
88+
}
6889

6990
let parse (output: MF.ConsoleApplication.Output) file =
7091
let checker = FSharpChecker.Create() // to allow implementation details add: keepAssemblyContents=true
@@ -73,4 +94,14 @@ module Parser =
7394

7495
(file, File.ReadAllText file)
7596
|> parseAndCheck output checker
76-
|> ParsedDomain
97+
|> AsyncResult.map ParsedDomain
98+
99+
let parseSequentualy output files =
100+
files
101+
|> List.map (parse output)
102+
|> AsyncResult.ofSequentialAsyncResults ParseError
103+
104+
let parseParallely output files =
105+
files
106+
|> List.map (parse output)
107+
|> AsyncResult.ofParallelAsyncResults ParseError

src/Domain/Resolver.fs

+47-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@ namespace Tuc.Domain
22

33
open FSharp.Compiler.Symbols
44

5+
[<RequireQualifiedAccess>]
6+
type ResolveError =
7+
| UnresolvedTypes of TypeName list
8+
9+
[<RequireQualifiedAccess>]
10+
type AsyncResolveError =
11+
| ParseError of ParseError list
12+
| UnresolvedTypes of TypeName list
13+
14+
[<RequireQualifiedAccess>]
15+
module internal AsyncResolveError =
16+
let ofResolveError = function
17+
| ResolveError.UnresolvedTypes types -> AsyncResolveError.UnresolvedTypes types
18+
519
[<RequireQualifiedAccess>]
620
module Resolver =
721
open Option.Operators
@@ -297,7 +311,7 @@ module Resolver =
297311
:: resolvedTypes
298312
|> output.Options (sprintf "Resolved (or scalar) types [%d]:" (resolvedTypes |> List.length))
299313

300-
let resolve output (parsedDomains: ParsedDomain list): Result<ResolvedType list, _> =
314+
let resolve output (parsedDomains: ParsedDomain list): Result<ResolvedType list, ResolveError> =
301315
let emptyResolvedTypes: ResolvedTypes = Map.empty
302316

303317
// resolve all scalar types in advance
@@ -331,4 +345,36 @@ module Resolver =
331345
| unresolvedTypes ->
332346
unresolvedTypes
333347
|> List.map ResolvedType.name
348+
|> ResolveError.UnresolvedTypes
334349
|> Error
350+
351+
let resolveOne output (parsedDomain: ParsedDomain): Result<ResolvedType list, ResolveError> =
352+
[ parsedDomain ]
353+
|> resolve output
354+
355+
open ErrorHandling
356+
open ErrorHandling.AsyncResult.Operators
357+
358+
let resolveOneAsync output (parseDomain: AsyncResult<ParsedDomain, ParseError>): AsyncResult<ResolvedType list, AsyncResolveError> =
359+
asyncResult {
360+
let! parsedDomain = parseDomain <@> (List.singleton >> AsyncResolveError.ParseError)
361+
362+
let! resolved =
363+
parsedDomain
364+
|> resolveOne output
365+
|> AsyncResult.ofResult <@> AsyncResolveError.ofResolveError
366+
367+
return resolved
368+
}
369+
370+
let resolveAsync output (parseDomains: AsyncResult<ParsedDomain list, ParseError list>): AsyncResult<ResolvedType list, AsyncResolveError> =
371+
asyncResult {
372+
let! parsedDomains = parseDomains <@> AsyncResolveError.ParseError
373+
374+
let! resolved =
375+
parsedDomains
376+
|> resolve output
377+
|> AsyncResult.ofResult <@> AsyncResolveError.ofResolveError
378+
379+
return resolved
380+
}

src/ErrorHandling/Result.fs

+68-29
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,46 @@ module AsyncResult =
373373
let ofTaskCatch f x : AsyncResult<_, _> =
374374
x |> ofTask |> catch f
375375

376+
/// Run asyncResults in Parallel, handles the errors and concats results
377+
let ofParallelAsyncResults<'Success, 'Error> (f: exn -> 'Error) (results: AsyncResult<'Success, 'Error> list): AsyncResult<'Success list, 'Error list> =
378+
results
379+
|> List.map (mapError List.singleton)
380+
|> Async.Parallel
381+
|> ofAsyncCatch (f >> List.singleton)
382+
|> bind (
383+
Seq.toList
384+
>> Validation.ofResults
385+
>> Result.mapError List.concat
386+
>> ofResult
387+
)
388+
389+
/// Run asyncs in Parallel, handles the errors and concats results
390+
let ofParallelAsyncs<'Success, 'Error> (f: exn -> 'Error) (asyncs: Async<'Success> list): AsyncResult<'Success list, 'Error list> =
391+
asyncs
392+
|> Async.Parallel
393+
|> ofAsyncCatch (f >> List.singleton)
394+
|> map Seq.toList
395+
396+
/// Run asyncResults in Parallel, handles the errors and concats results
397+
let ofSequentialAsyncResults<'Success, 'Error> (f: exn -> 'Error) (results: AsyncResult<'Success, 'Error> list): AsyncResult<'Success list, 'Error list> =
398+
results
399+
|> List.map (mapError List.singleton)
400+
|> Async.Sequential
401+
|> ofAsyncCatch (f >> List.singleton)
402+
|> bind (
403+
Seq.toList
404+
>> Validation.ofResults
405+
>> Result.mapError List.concat
406+
>> ofResult
407+
)
408+
409+
/// Run asyncs in Parallel, handles the errors and concats results
410+
let ofSequentialAsyncs<'Success, 'Error> (f: exn -> 'Error) (asyncs: Async<'Success> list): AsyncResult<'Success list, 'Error list> =
411+
asyncs
412+
|> Async.Sequential
413+
|> ofAsyncCatch (f >> List.singleton)
414+
|> map Seq.toList
415+
376416
//-----------------------------------
377417
// Utilities lifted from Async
378418

@@ -405,44 +445,43 @@ module AsyncResult =
405445
/// The `asyncResult` computation expression is available globally without qualification
406446
[<AutoOpen>]
407447
module AsyncResultComputationExpression =
448+
open System
408449

409450
type AsyncResultBuilder() =
410-
member __.Return(x) = AsyncResult.retn x
411-
member __.Bind(x, f) = AsyncResult.bind f x
451+
member __.Return (value: 'Success): AsyncResult<'Success, 'Error> =
452+
async.Return <| result.Return value
412453

413-
member __.ReturnFrom(x) = x
414-
member this.Zero() = this.Return ()
454+
member __.ReturnFrom(asyncResult: AsyncResult<'Success, 'Error>): AsyncResult<'Success, 'Error> =
455+
asyncResult
415456

416-
member __.Delay(f) = f
417-
member __.Run(f) = f()
457+
member __.Zero (): AsyncResult<unit, 'Error> =
458+
async.Return <| result.Zero ()
418459

419-
member this.While(guard, body) =
420-
if not (guard())
421-
then this.Zero()
422-
else this.Bind( body(), fun () ->
423-
this.While(guard, body))
460+
member __.Bind (asyncResult: AsyncResult<'SuccessA, 'Error>, (f: 'SuccessA -> AsyncResult<'SuccessB, 'Error>)): AsyncResult<'SuccessB, 'Error> =
461+
asyncResult |> AsyncResult.bind f
424462

425-
member this.TryWith(body, handler) =
426-
try this.ReturnFrom(body())
427-
with e -> handler e
463+
member __.Delay (generator: unit -> AsyncResult<'Success, 'Error>): AsyncResult<'Success, 'Error> =
464+
async.Delay generator
428465

429-
member this.TryFinally(body, compensation) =
430-
try this.ReturnFrom(body())
431-
finally compensation()
466+
member this.Combine (computation1: AsyncResult<unit, 'Error>, computation2: AsyncResult<'U, 'Error>): AsyncResult<'U, 'Error> =
467+
this.Bind(computation1, fun () -> computation2)
432468

433-
member this.Using(disposable:#System.IDisposable, body) =
434-
let body' = fun () -> body disposable
435-
this.TryFinally(body', fun () ->
436-
match disposable with
437-
| null -> ()
438-
| disp -> disp.Dispose())
469+
member __.TryWith (computation: AsyncResult<'Success, 'Error>, handler: exn -> AsyncResult<'Success, 'Error>): AsyncResult<'Success, 'Error> =
470+
async.TryWith(computation, handler)
439471

440-
member this.For(sequence:seq<_>, body) =
441-
this.Using(sequence.GetEnumerator(),fun enum ->
442-
this.While(enum.MoveNext,
443-
this.Delay(fun () -> body enum.Current)))
472+
member __.TryFinally (computation: AsyncResult<'Success, 'Error>, compensation: unit -> unit): AsyncResult<'Success, 'Error> =
473+
async.TryFinally(computation, compensation)
444474

445-
member this.Combine (a,b) =
446-
this.Bind(a, fun () -> b())
475+
member __.Using (resource: 'SuccessA when 'SuccessA :> IDisposable, binder: 'SuccessA -> AsyncResult<'SuccessB, 'Error>): AsyncResult<'SuccessB, 'Error> =
476+
async.Using(resource, binder)
477+
478+
member this.While (guard: unit -> bool, computation: AsyncResult<unit, 'Error>): AsyncResult<unit, 'Error> =
479+
if not <| guard () then this.Zero ()
480+
else this.Bind(computation, fun () -> this.While (guard, computation))
481+
482+
member this.For (sequence: #seq<'Success>, binder: 'Success -> AsyncResult<unit, 'Error>): AsyncResult<unit, 'Error> =
483+
this.Using(sequence.GetEnumerator (), fun enum ->
484+
this.While(enum.MoveNext,
485+
this.Delay(fun () -> binder enum.Current)))
447486

448487
let asyncResult = AsyncResultBuilder()

tests/Domain/ResolverTest.fs

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module Tuc.Domain.Test.Parser
22

33
open Expecto
44
open System.IO
5+
open ErrorHandling
56

67
let (</>) a b = Path.Combine(a, b)
78

@@ -41,7 +42,7 @@ module Domain =
4142
type Case = {
4243
Description: string
4344
Domain: string
44-
Expected: Result<ResolvedType list, TypeName list>
45+
Expected: Result<ResolvedType list, AsyncResolveError>
4546
}
4647

4748
let case description domain expected =
@@ -263,8 +264,8 @@ module Domain =
263264
let resolvedDomain =
264265
domain
265266
|> Parser.parse output
266-
|> List.singleton
267-
|> Resolver.resolve output
267+
|> Resolver.resolveOneAsync output
268+
|> Async.RunSynchronously
268269

269270
match expected, resolvedDomain with
270271
| Ok expected, Ok actual -> Expect.equal (actual |> List.sort) (expected |> List.sort) description

0 commit comments

Comments
 (0)