Skip to content

Commit af90106

Browse files
dbrattliclaude
andauthored
feat: tighten typing with phantom params, Dynamic, and decoders (#80)
* feat: tighten typing with phantom params, Dynamic, and decoders Systematically replace `obj` in binding signatures with stronger types so the F# type system can catch mismatches at compile time. The changes fall into five themes: Phantom-typed opaque handles (Gleam Subject analogue, zero runtime cost): - Pid<'Msg>, Ref<'Tag>, TimerRef<'Msg> - TableId<'Key,'Row>, ServerRef<'Call,'Cast> - Req as [<Erase>] DU (was a bare `type Req = obj` alias) Erased DUs with typed Emit constructors: - HandlerResult<'State>, WsResult<'State> for Cowboy handler returns - WsFrame with text/binary/ping/pong/close constructors - ServerName with localName/globalName/viaName Plain F# DUs for discrete atom sets (verified: DU cases compile to Erlang atoms on Fable BEAM, with [<CompiledName>] handling multi-word snake_case atoms): - RandAlg in Rand.fs - EtsTableType (Set/OrderedSet/Bag/DuplicateBag), EtsAccess in Ets.fs - Typed ets:info/2 accessors: size, tableName, tableType, access, owner, memory, keypos Dynamic type + Decode module for values whose shape is not known statically. Gleam-style combinators with System.Func callbacks (the latter avoids a Fable positional-arg bug with curried function-valued parameters): - Decode.int/float/bool/atom/string/dynamic - Decode.field/list/optional/tuple2 - Decode.succeed/map/andThen - Applied to Erlang.binaryToTerm and Application.getEnv Tighter signatures across the board: - Erlang: typed send, exit, monitor, whereis, get/put/erase (process dict), length, atomToList/listToAtom, exactEquals, termToBinary - GenServer: generic 'Args/'Call/'Cast/'Reply - Logger: U2<BeamMap<Atom,obj>, obj list> for metadataOrArgs - Application: typed Result wrappers for start/stop/ensureAllStarted - Cowboy: Atom listener names, BeamMap headers, typed binding Atom key Structural changes: - Added Fable.Beam.Cowboy -> Fable.Beam project reference so Cowboy can use Atom/BeamMap/etc. - Reordered src/Fable.Beam.fsproj so Maps.fs, Lists.fs, Dynamic.fs come before modules that depend on them. Testing: 283/283 passing (267 start + 16 new for Dynamic, Decode, StringEnum-via-DU canaries, ETS info accessors). Every step verified against `just test-dotnet` + `just test`. Generated Erlang inspected for key changes to confirm zero runtime impact. BINDINGS-GUIDE.md updated with a "Core Rule: Avoid obj" section, new sub-sections on phantom typing, DU-as-atom emission, Dynamic + decoders, the System.Func/curried-Emit gotcha, and a full Anti-patterns section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style: apply Fantomas formatting Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 917f177 commit af90106

26 files changed

Lines changed: 1001 additions & 218 deletions

BINDINGS-GUIDE.md

Lines changed: 398 additions & 75 deletions
Large diffs are not rendered by default.

src/Fable.Beam.fsproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@
1212
</PropertyGroup>
1313
<ItemGroup>
1414
<Compile Include="otp/Types.fs" />
15+
<Compile Include="otp/Maps.fs" />
16+
<Compile Include="otp/Lists.fs" />
17+
<Compile Include="otp/Dynamic.fs" />
1518
<Compile Include="otp/Erlang.fs" />
1619
<Compile Include="otp/GenServer.fs" />
1720
<Compile Include="otp/Supervisor.fs" />
1821
<Compile Include="otp/Timer.fs" />
1922
<Compile Include="otp/Io.fs" />
2023
<Compile Include="otp/Logger.fs" />
2124
<Compile Include="otp/Ets.fs" />
22-
<Compile Include="otp/Maps.fs" />
23-
<Compile Include="otp/Lists.fs" />
2425
<Compile Include="otp/Init.fs" />
2526
<Compile Include="otp/Application.fs" />
2627
<Compile Include="otp/Binary.fs" />

src/cowboy/Cowboy.fs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,24 @@
33
module Fable.Beam.Cowboy.Cowboy
44

55
open Fable.Core
6+
open Fable.Beam
67

78
/// Start a clear (HTTP) listener.
89
[<Emit("cowboy:start_clear($0, $1, $2)")>]
9-
let startClear (name: obj) (transportOpts: obj) (protoOpts: obj) : obj = nativeOnly
10+
let startClear (name: Atom) (transportOpts: obj) (protoOpts: obj) : obj = nativeOnly
1011

1112
/// Start a TLS (HTTPS) listener.
1213
[<Emit("cowboy:start_tls($0, $1, $2)")>]
13-
let startTls (name: obj) (transportOpts: obj) (protoOpts: obj) : obj = nativeOnly
14+
let startTls (name: Atom) (transportOpts: obj) (protoOpts: obj) : obj = nativeOnly
1415

1516
/// Stop a running listener.
1617
[<Emit("cowboy:stop_listener($0)")>]
17-
let stopListener (name: obj) : obj = nativeOnly
18+
let stopListener (name: Atom) : obj = nativeOnly
1819

1920
/// Retrieve a listener's environment value.
2021
[<Emit("cowboy:get_env($0, $1)")>]
21-
let getEnv (name: obj) (key: obj) : obj = nativeOnly
22+
let getEnv (name: Atom) (key: Atom) : obj = nativeOnly
2223

2324
/// Update a listener's environment value.
2425
[<Emit("cowboy:set_env($0, $1, $2)")>]
25-
let setEnv (name: obj) (key: obj) (value: obj) : obj = nativeOnly
26+
let setEnv (name: Atom) (key: Atom) (value: obj) : obj = nativeOnly

src/cowboy/CowboyHandler.fs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ module Fable.Beam.Cowboy.CowboyHandler
44

55
open Fable.Core
66

7+
/// Erased wrapper for a handler callback's return value.
8+
/// The phantom 'State captures the handler state threaded through callbacks.
9+
[<Erase>]
10+
type HandlerResult<'State> = HandlerResult of obj
11+
712
/// Return {ok, Req, State} from init/2.
813
[<Emit("{ok, $0, $1}")>]
9-
let ok (req: obj) (state: obj) : obj = nativeOnly
14+
let ok (req: CowboyReq.Req) (state: 'State) : HandlerResult<'State> = nativeOnly

src/cowboy/CowboyReq.fs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
module Fable.Beam.Cowboy.CowboyReq
44

55
open Fable.Core
6+
open Fable.Beam
7+
open Fable.Beam.Maps
68

7-
/// Opaque Cowboy request object.
8-
type Req = obj
9+
/// Opaque Cowboy request object. Erased at runtime — prevents accidental
10+
/// mixing with other untyped Erlang terms.
11+
[<Erase>]
12+
type Req = Req of obj
913

1014
/// Get the HTTP method (e.g. <<"GET">>, <<"POST">>).
1115
[<Emit("cowboy_req:method($0)")>]
@@ -23,9 +27,9 @@ let header (name: string) (req: Req) : string = nativeOnly
2327
[<Emit("cowboy_req:header($0, $1, $2)")>]
2428
let headerDefault (name: string) (req: Req) (defaultValue: string) : string = nativeOnly
2529

26-
/// Get all request headers as a map.
30+
/// Get all request headers as a map of binary-keyed strings.
2731
[<Emit("cowboy_req:headers($0)")>]
28-
let headers (req: Req) : obj = nativeOnly
32+
let headers (req: Req) : BeamMap<string, string> = nativeOnly
2933

3034
/// Read the full request body. Returns {ok, Body, Req}.
3135
[<Emit("cowboy_req:read_body($0)")>]
@@ -37,11 +41,11 @@ let queryString (req: Req) : string = nativeOnly
3741

3842
/// Send a full response: status, headers map, body, req.
3943
[<Emit("cowboy_req:reply($0, $1, $2, $3)")>]
40-
let reply (status: int) (headers: obj) (body: string) (req: Req) : Req = nativeOnly
44+
let reply (status: int) (headers: BeamMap<string, string>) (body: string) (req: Req) : Req = nativeOnly
4145

4246
/// Send a response with status and headers only (no body).
4347
[<Emit("cowboy_req:reply($0, $1, $2)")>]
44-
let replyNoBody (status: int) (headers: obj) (req: Req) : Req = nativeOnly
48+
let replyNoBody (status: int) (headers: BeamMap<string, string>) (req: Req) : Req = nativeOnly
4549

4650
/// Get the peer IP address and port.
4751
[<Emit("cowboy_req:peer($0)")>]
@@ -65,8 +69,8 @@ let parseQs (req: Req) : obj = nativeOnly
6569

6670
/// Get a binding value from the route.
6771
[<Emit("cowboy_req:binding($0, $1)")>]
68-
let binding (name: obj) (req: Req) : obj = nativeOnly
72+
let binding (name: Atom) (req: Req) : string = nativeOnly
6973

7074
/// Get a binding value with a default.
7175
[<Emit("cowboy_req:binding($0, $1, $2)")>]
72-
let bindingDefault (name: obj) (req: Req) (defaultValue: obj) : obj = nativeOnly
76+
let bindingDefault (name: Atom) (req: Req) (defaultValue: string) : string = nativeOnly

src/cowboy/CowboyRouter.fs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
module Fable.Beam.Cowboy.CowboyRouter
44

55
open Fable.Core
6+
open Fable.Beam
67

78
/// Compile routing rules into a dispatch list.
89
[<Emit("cowboy_router:compile($0)")>]
910
let compile (routes: obj) : obj = nativeOnly
1011

1112
/// Route: {Path, Handler, InitialState}.
13+
/// Handler is the module atom that implements the cowboy_handler behaviour.
1214
[<Emit("{$0, $1, $2}")>]
13-
let route (path: string) (handler: obj) (state: obj) : obj = nativeOnly
15+
let route (path: string) (handler: Atom) (state: 'State) : obj = nativeOnly
1416

1517
/// Host rule: {HostMatch, Routes}.
18+
/// HostMatch is typically a binary pattern or the atom '_' for wildcard.
1619
[<Emit("{$0, $1}")>]
1720
let hostRule (host: obj) (routes: obj list) : obj = nativeOnly

src/cowboy/CowboyWebsocket.fs

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,59 @@ module Fable.Beam.Cowboy.CowboyWebsocket
44

55
open Fable.Core
66

7+
/// Erased wrapper for a WebSocket frame. Use the helpers below to construct.
8+
[<Erase>]
9+
type WsFrame = WsFrame of obj
10+
11+
/// Erased wrapper for a WebSocket callback's return value.
12+
/// The phantom 'State captures the handler state.
13+
[<Erase>]
14+
type WsResult<'State> = WsResult of obj
15+
16+
// -- Frame constructors --
17+
18+
/// Create a text frame: {text, Data}.
19+
[<Emit("{text, $0}")>]
20+
let textFrame (data: string) : WsFrame = nativeOnly
21+
22+
/// Create a binary frame: {binary, Data}.
23+
[<Emit("{binary, $0}")>]
24+
let binaryFrame (data: byte array) : WsFrame = nativeOnly
25+
26+
/// Create a ping frame.
27+
[<Emit("ping")>]
28+
let pingFrame: WsFrame = nativeOnly
29+
30+
/// Create a pong frame.
31+
[<Emit("pong")>]
32+
let pongFrame: WsFrame = nativeOnly
33+
34+
/// Create a close frame: {close, Code, Reason}.
35+
[<Emit("{close, $0, $1}")>]
36+
let closeFrame (code: int) (reason: string) : WsFrame = nativeOnly
37+
738
// -- Handler return types --
839

940
/// Return {cowboy_websocket, Req, State} from init/2 to upgrade to WebSocket.
1041
[<Emit("{cowboy_websocket, $0, $1}")>]
11-
let upgrade (req: obj) (state: obj) : obj = nativeOnly
42+
let upgrade (req: CowboyReq.Req) (state: 'State) : WsResult<'State> = nativeOnly
1243

1344
/// Return {cowboy_websocket, Req, State, Opts} from init/2 with options.
1445
[<Emit("{cowboy_websocket, $0, $1, $2}")>]
15-
let upgradeWithOpts (req: obj) (state: obj) (opts: obj) : obj = nativeOnly
46+
let upgradeWithOpts (req: CowboyReq.Req) (state: 'State) (opts: obj) : WsResult<'State> = nativeOnly
1647

1748
/// Return {ok, State} from websocket_init/1 or websocket_handle/2.
1849
[<Emit("{ok, $0}")>]
19-
let ok (state: obj) : obj = nativeOnly
50+
let ok (state: 'State) : WsResult<'State> = nativeOnly
2051

2152
/// Return {reply, Frame, State} to send a single frame.
2253
[<Emit("{reply, $0, $1}")>]
23-
let reply (frame: obj) (state: obj) : obj = nativeOnly
54+
let reply (frame: WsFrame) (state: 'State) : WsResult<'State> = nativeOnly
2455

2556
/// Return {reply, Frames, State} to send multiple frames.
2657
[<Emit("{reply, $0, $1}")>]
27-
let replyMany (frames: obj list) (state: obj) : obj = nativeOnly
58+
let replyMany (frames: WsFrame list) (state: 'State) : WsResult<'State> = nativeOnly
2859

2960
/// Return {stop, State} to close the WebSocket connection.
3061
[<Emit("{stop, $0}")>]
31-
let stop (state: obj) : obj = nativeOnly
32-
33-
// -- Frame constructors --
34-
35-
/// Create a text frame: {text, Data}.
36-
[<Emit("{text, $0}")>]
37-
let textFrame (data: string) : obj = nativeOnly
38-
39-
/// Create a binary frame: {binary, Data}.
40-
[<Emit("{binary, $0}")>]
41-
let binaryFrame (data: obj) : obj = nativeOnly
42-
43-
/// Create a ping frame.
44-
[<Emit("ping")>]
45-
let pingFrame: obj = nativeOnly
46-
47-
/// Create a pong frame.
48-
[<Emit("pong")>]
49-
let pongFrame: obj = nativeOnly
50-
51-
/// Create a close frame: {close, Code, Reason}.
52-
[<Emit("{close, $0, $1}")>]
53-
let closeFrame (code: int) (reason: string) : obj = nativeOnly
62+
let stop (state: 'State) : WsResult<'State> = nativeOnly

src/cowboy/Fable.Beam.Cowboy.fsproj

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@
1111
<Description>Fable bindings for Cowboy HTTP server on BEAM</Description>
1212
</PropertyGroup>
1313
<ItemGroup>
14-
<Compile Include="Cowboy.fs" />
15-
<Compile Include="CowboyHandler.fs" />
14+
<ProjectReference Include="..\Fable.Beam.fsproj" />
15+
</ItemGroup>
16+
<ItemGroup>
1617
<Compile Include="CowboyReq.fs" />
17-
<Compile Include="CowboyRouter.fs" />
18+
<Compile Include="CowboyHandler.fs" />
1819
<Compile Include="CowboyWebsocket.fs" />
20+
<Compile Include="CowboyRouter.fs" />
21+
<Compile Include="Cowboy.fs" />
1922
</ItemGroup>
2023
<ItemGroup>
2124
<Content Include="*.fsproj; **\*.fs; **\*.fsi" PackagePath="fable\" />

src/otp/Application.fs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ open Fable.Beam
77

88
// fsharplint:disable MemberNames
99

10+
/// Raw low-level bindings. Prefer the typed helpers below for most use cases.
1011
[<Erase>]
1112
type IExports =
1213
/// Starts an application and all its dependencies.
@@ -18,12 +19,36 @@ type IExports =
1819
/// Gets an application environment variable.
1920
abstract get_env: app: Atom * key: Atom -> obj
2021
/// Gets an application environment variable with default.
21-
abstract get_env: app: Atom * key: Atom * defaultValue: obj -> obj
22+
abstract get_env: app: Atom * key: Atom * defaultValue: 'V -> 'V
2223
/// Sets an application environment variable.
23-
abstract set_env: app: Atom * key: Atom * value: obj -> obj
24+
abstract set_env: app: Atom * key: Atom * value: 'V -> unit
2425
/// Returns a list of all running applications.
2526
abstract which_applications: unit -> obj
2627

2728
/// application module
2829
[<ImportAll("application")>]
2930
let application: IExports = nativeOnly
31+
32+
// ============================================================================
33+
// Typed API
34+
// ============================================================================
35+
// WORKAROUND: Emit expressions wrapped in (fun() -> ... end)() to prevent
36+
// Erlang "unsafe variable" errors. Remove IIFEs once fixed in Fable.
37+
38+
/// Gets an application environment variable. Returns None if unset.
39+
/// The returned Dynamic must be decoded with `Fable.Beam.Decode` combinators.
40+
[<Emit("(fun() -> case application:get_env($0, $1) of {ok, GetEnvVal__} -> GetEnvVal__; undefined -> undefined end end)()")>]
41+
let getEnv (app: Atom) (key: Atom) : Dynamic option = nativeOnly
42+
43+
/// Starts an application and all its dependencies. Returns the list of apps
44+
/// that were started (empty if everything was already running).
45+
[<Emit("(fun() -> case application:ensure_all_started($0) of {ok, Started__} -> {ok, fable_utils:new_ref(Started__)}; {error, Reason__} -> {error, erlang:list_to_binary(io_lib:format(<<\"~p\">>, [Reason__]))} end end)()")>]
46+
let ensureAllStarted (app: Atom) : Result<Atom array, string> = nativeOnly
47+
48+
/// Starts an application (without dependencies). Returns Ok on success.
49+
[<Emit("(fun() -> case application:start($0) of ok -> {ok, ok}; {error, Reason__} -> {error, erlang:list_to_binary(io_lib:format(<<\"~p\">>, [Reason__]))} end end)()")>]
50+
let start (app: Atom) : Result<unit, string> = nativeOnly
51+
52+
/// Stops an application. Returns Ok on success.
53+
[<Emit("(fun() -> case application:stop($0) of ok -> {ok, ok}; {error, Reason__} -> {error, erlang:list_to_binary(io_lib:format(<<\"~p\">>, [Reason__]))} end end)()")>]
54+
let stop (app: Atom) : Result<unit, string> = nativeOnly

src/otp/Dynamic.fs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/// A dynamically-typed Erlang term with composable decoders for extracting
2+
/// typed values. Use this instead of bare `obj` when the runtime shape is
3+
/// unknown and must be validated.
4+
///
5+
/// Inspired by Gleam's `gleam/dynamic/decode` module.
6+
namespace Fable.Beam
7+
8+
open Fable.Core
9+
10+
/// An Erlang term of unknown static type. Construct from untyped Erlang
11+
/// responses and narrow with the combinators in the `Decode` module.
12+
[<Erase>]
13+
type Dynamic = Dynamic of obj
14+
15+
/// Decoder combinators for extracting typed values from a `Dynamic`.
16+
/// Each decoder returns `Result<'T, string>` where the error is a human-readable message.
17+
///
18+
/// WORKAROUND: Emit expressions wrapped in `(fun() -> ... end)()` to prevent
19+
/// Erlang "unsafe variable" errors. Remove IIFEs once fixed in Fable.
20+
module Decode =
21+
22+
/// Decode a Dynamic as an integer.
23+
[<Emit("(fun() -> case erlang:is_integer($0) of true -> {ok, $0}; false -> {error, <<\"expected integer\">>} end end)()")>]
24+
let int (d: Dynamic) : Result<int, string> = nativeOnly
25+
26+
/// Decode a Dynamic as a float.
27+
[<Emit("(fun() -> case erlang:is_float($0) of true -> {ok, $0}; false -> {error, <<\"expected float\">>} end end)()")>]
28+
let float (d: Dynamic) : Result<float, string> = nativeOnly
29+
30+
/// Decode a Dynamic as a boolean (atoms `true` or `false`).
31+
[<Emit("(fun() -> case erlang:is_boolean($0) of true -> {ok, $0}; false -> {error, <<\"expected boolean\">>} end end)()")>]
32+
let bool (d: Dynamic) : Result<bool, string> = nativeOnly
33+
34+
/// Decode a Dynamic as an atom.
35+
[<Emit("(fun() -> case erlang:is_atom($0) of true -> {ok, $0}; false -> {error, <<\"expected atom\">>} end end)()")>]
36+
let atom (d: Dynamic) : Result<Atom, string> = nativeOnly
37+
38+
/// Decode a Dynamic as a binary string.
39+
[<Emit("(fun() -> case erlang:is_binary($0) of true -> {ok, $0}; false -> {error, <<\"expected binary string\">>} end end)()")>]
40+
let string (d: Dynamic) : Result<string, string> = nativeOnly
41+
42+
/// Return the Dynamic unchanged. Useful as a terminal decoder in `field`
43+
/// chains when you want to hand off further decoding to the caller.
44+
[<Emit("{ok, $0}")>]
45+
let dynamic (d: Dynamic) : Result<Dynamic, string> = nativeOnly
46+
47+
/// Extract a field from an Erlang map and decode it with the given decoder.
48+
/// Returns Error if the map doesn't contain the key or the inner decoder fails.
49+
/// Uses System.Func for the decoder — matches the Lists.fs convention for
50+
/// callbacks and avoids curried-arg substitution issues with Emit.
51+
[<Emit("(fun() -> case maps:find($0, $2) of {ok, FieldVal__} -> $1(FieldVal__); error -> {error, erlang:list_to_binary(io_lib:format(<<\"missing field ~p\">>, [$0]))} end end)()")>]
52+
let field (key: Atom) (decoder: System.Func<Dynamic, Result<'V, string>>) (d: Dynamic) : Result<'V, string> =
53+
nativeOnly
54+
55+
/// Decode a Dynamic as a list of values, decoding each element with `decoder`.
56+
/// Short-circuits on the first decode error.
57+
[<Emit("(fun() -> case erlang:is_list($1) of true -> DecodeListFold__ = fun (_, {error, _} = E__) -> E__; (Elem__, {ok, Acc__}) -> case $0(Elem__) of {ok, V__} -> {ok, [V__ | Acc__]}; {error, _} = E__ -> E__ end end, case lists:foldl(DecodeListFold__, {ok, []}, $1) of {ok, Rev__} -> {ok, fable_utils:new_ref(lists:reverse(Rev__))}; {error, _} = E__ -> E__ end; false -> {error, <<\"expected list\">>} end end)()")>]
58+
let list (decoder: System.Func<Dynamic, Result<'V, string>>) (d: Dynamic) : Result<'V array, string> = nativeOnly
59+
60+
/// Decode a Dynamic that may be the atom `undefined` (mapped to None) or
61+
/// a value decoded by the inner decoder (mapped to Some).
62+
[<Emit("(fun() -> case $1 of undefined -> {ok, undefined}; V__ -> case $0(V__) of {ok, V2__} -> {ok, V2__}; {error, _} = E__ -> E__ end end end)()")>]
63+
let optional (decoder: System.Func<Dynamic, Result<'V, string>>) (d: Dynamic) : Result<'V option, string> =
64+
nativeOnly
65+
66+
/// Decode a 2-tuple by applying each decoder to the corresponding element.
67+
[<Emit("(fun() -> case erlang:is_tuple($2) andalso erlang:tuple_size($2) =:= 2 of true -> case $0(erlang:element(1, $2)) of {ok, A__} -> case $1(erlang:element(2, $2)) of {ok, B__} -> {ok, {A__, B__}}; {error, _} = E__ -> E__ end; {error, _} = E__ -> E__ end; false -> {error, <<\"expected 2-tuple\">>} end end)()")>]
68+
let tuple2
69+
(decA: System.Func<Dynamic, Result<'A, string>>)
70+
(decB: System.Func<Dynamic, Result<'B, string>>)
71+
(d: Dynamic)
72+
: Result<'A * 'B, string> =
73+
nativeOnly
74+
75+
/// Lift a plain value into the Result monad. Useful for building records
76+
/// after extracting all fields successfully.
77+
let inline succeed (value: 'V) : Result<'V, string> = Ok value
78+
79+
/// Map the successful value of a decode result through a function.
80+
let inline map (f: 'A -> 'B) (r: Result<'A, string>) : Result<'B, string> = Result.map f r
81+
82+
/// Chain decode results: run the first, and if it succeeds, run the second
83+
/// with its value.
84+
let inline andThen (f: 'A -> Result<'B, string>) (r: Result<'A, string>) : Result<'B, string> = Result.bind f r

0 commit comments

Comments
 (0)