git.iko.soy/rowtype-yoga/purescript-yoga-om-layer
on main help sign in
Add runScopedWith/withScopedWith and error channel tests for scoped layers
commitb070f4373b0bb7cfbd072f632d8e2d9cce3cfa56 authorMark <mark.eibes@gmail.com> dateFri 13 Feb 2026, 22:18 (8 months ago) parent05f8bee
4 files changed+173−73
.psc-ide-port+1−1 src/Yoga/Om/Layer.purs+66−23 test/Playground.purs+4−4 test/Test/Main.purs+102−45
.psc-ide-port+1−1
@@ -1 +1 @@
1−15820
1+15887
src/Yoga/Om/Layer.purs+66−23
@@ -1,6 +1,7 @@
11-- @inline export runLayer arity=2
22module Yoga.Om.Layer
33 ( OmLayer
4+ , ParOmLayer
45 , Scope
56 , makeLayer
67 , makeScopedLayer
@@ -22,6 +23,8 @@ module Yoga.Om.Layer
2223
2324import Prelude
2425
26+import Control.Parallel (class Parallel)
27+import Control.Parallel as Parallel
2528import Data.Array as Array
2629import Data.Foldable (for_)
2730import Data.Map (Map)
@@ -43,7 +46,7 @@ import Record as Record
4346import Record.Studio (class Keys)
4447import Type.Equality (class TypeEquals)
4548import Unsafe.Coerce (unsafeCoerce)
46−import Yoga.Om (Om)
49+import Yoga.Om (Om, ParOm)
4750import Yoga.Om as Om
4851
4952-- =============================================================================
@@ -102,9 +105,49 @@ freshId _ = unsafePerformEffect nextId
102105-- OmLayer
103106-- =============================================================================
104107
105−-- | A layer that requires dependencies (req) and provides services (prov).
108+-- | A layer that requires dependencies (req), may fail with (err),
109+-- | and produces a value (typically Record prov).
106110-- | Each layer has a unique identity used for automatic memoization within a Scope.
107−data OmLayer req prov err = OmLayer Int (Om (Record req) err (Record prov))
111+data OmLayer req err a = OmLayer Int (Om (Record req) err a)
112+
113+-- =============================================================================
114+-- ParOmLayer — parallel counterpart
115+-- =============================================================================
116+
117+data ParOmLayer req err a = ParOmLayer (ParOm (Record req) err a)
118+
119+-- =============================================================================
120+-- Instances
121+-- =============================================================================
122+
123+instance Functor (OmLayer req err) where
124+ map f layer = OmLayer (freshId unit) (map f (buildLayer layer))
125+
126+instance Apply (OmLayer req err) where
127+ apply f a = OmLayer (freshId unit) (apply (buildLayer f) (buildLayer a))
128+
129+instance Applicative (OmLayer req err) where
130+ pure a = OmLayer (freshId unit) (pure a)
131+
132+instance Bind (OmLayer req err) where
133+ bind layer f = OmLayer (freshId unit) do
134+ a <- buildLayer layer
135+ buildLayer (f a)
136+
137+instance Monad (OmLayer req err)
138+
139+instance Functor (ParOmLayer req err) where
140+ map f (ParOmLayer p) = ParOmLayer (map f p)
141+
142+instance Apply (ParOmLayer req err) where
143+ apply (ParOmLayer f) (ParOmLayer a) = ParOmLayer (apply f a)
144+
145+instance Applicative (ParOmLayer req err) where
146+ pure a = ParOmLayer (pure a)
147+
148+instance Parallel (ParOmLayer req err) (OmLayer req err) where
149+ parallel (OmLayer _ om) = ParOmLayer (Parallel.parallel om)
150+ sequential (ParOmLayer p) = OmLayer (freshId unit) (Parallel.sequential p)
108151
109152-- =============================================================================
110153-- Automatic memoization
@@ -113,7 +156,7 @@ data OmLayer req prov err = OmLayer Int (Om (Record req) err (Record prov))
113156-- | Wrap an Om computation with cache logic.
114157-- | If a scope is available in the context, checks/populates the cache by layer ID.
115158-- | If no scope is present, runs the computation directly.
116−withCache :: forall req prov err. Int -> Om (Record req) err (Record prov) -> Om (Record req) err (Record prov)
159+withCache :: forall req err a. Int -> Om (Record req) err a -> Om (Record req) err a
117160withCache layerId om = do
118161 ctx <- Om.ask
119162 case toMaybe (tryGetScope ctx) of
@@ -134,11 +177,11 @@ withCache layerId om = do
134177-- | Create a layer from an Om computation.
135178-- | The layer is automatically memoized within a Scope — the same layer value
136179-- | used in multiple branches of a dependency graph builds only once.
137−makeLayer :: forall req prov err. Om (Record req) err (Record prov) -> OmLayer req prov err
180+makeLayer :: forall req err a. Om (Record req) err a -> OmLayer req err a
138181makeLayer om = OmLayer (freshId unit) om
139182
140183-- | Extract the Om from a layer, wrapped with cache logic.
141−buildLayer :: forall req prov err. OmLayer req prov err -> Om (Record req) err (Record prov)
184+buildLayer :: forall req err a. OmLayer req err a -> Om (Record req) err a
142185buildLayer (OmLayer layerId om) = withCache layerId om
143186
144187-- | Create a scoped layer with acquire/release semantics.
@@ -148,7 +191,7 @@ makeScopedLayer
148191 :: forall req prov err
149192 . Om { scope :: Scope | req } err (Record prov)
150193 -> (Record prov -> Aff Unit)
151− -> OmLayer (scope :: Scope | req) prov err
194+ -> OmLayer (scope :: Scope | req) err (Record prov)
152195makeScopedLayer acquire release = makeLayer do
153196 acquireRelease acquire release
154197
@@ -159,7 +202,7 @@ bracketLayer
159202 . Om { scope :: Scope | req } err resource
160203 -> (resource -> Aff Unit)
161204 -> (resource -> Om { scope :: Scope | req } err (Record prov))
162− -> OmLayer (scope :: Scope | req) prov err
205+ -> OmLayer (scope :: Scope | req) err (Record prov)
163206bracketLayer acquire release use = makeLayer do
164207 resource <- acquireRelease acquire (\r -> release r)
165208 use resource
@@ -181,7 +224,7 @@ acquireRelease acquire release = do
181224-- | Create a fresh (non-memoized) copy of a layer.
182225-- | The new layer has a unique identity and will always build independently,
183226-- | even within the same scope.
184−fresh :: forall req prov err. OmLayer req prov err -> OmLayer req prov err
227+fresh :: forall req err a. OmLayer req err a -> OmLayer req err a
185228fresh (OmLayer _ om) = OmLayer (freshId unit) om
186229
187230-- =============================================================================
@@ -260,14 +303,14 @@ else instance
260303
261304-- | Run a layer with a given context, with custom error if requirements aren't met.
262305runLayer
263− :: forall req prov err available
306+ :: forall req err a available
264307 . CheckAllProvided req available
265308 => Record available
266− -> OmLayer req prov err
267− -> Om (Record available) err (Record prov)
309+ -> OmLayer req err a
310+ -> Om (Record available) err a
268311runLayer _ctx layer = widenCtx (buildLayer layer)
269312 where
270− widenCtx :: Om (Record req) err (Record prov) -> Om (Record available) err (Record prov)
313+ widenCtx :: Om (Record req) err a -> Om (Record available) err a
271314 widenCtx = unsafeCoerce
272315
273316-- | Build a fully-provided scoped layer, return the provisions.
@@ -275,7 +318,7 @@ runLayer _ctx layer = widenCtx (buildLayer layer)
275318-- | For layers with typed errors, use `runScopedWith`.
276319runScoped
277320 :: forall prov
278− . OmLayer (scope :: Scope) prov ()
321+ . OmLayer (scope :: Scope) () (Record prov)
279322 -> Aff (Record prov)
280323runScoped layer = withScoped layer pure
281324
@@ -286,7 +329,7 @@ runScoped layer = withScoped layer pure
286329-- | For layers with typed errors, use `withScopedWith`.
287330withScoped
288331 :: forall prov a
289− . OmLayer (scope :: Scope) prov ()
332+ . OmLayer (scope :: Scope) () (Record prov)
290333 -> (Record prov -> Aff a)
291334 -> Aff a
292335withScoped = withScopedWith { exception: \_ -> pure (unsafeCoerce {}) }
@@ -298,7 +341,7 @@ runScopedWith
298341 => VariantMatchCases rl err_ (Aff (Record prov))
299342 => Union err_ () (exception :: Error | err)
300343 => { exception :: Error -> Aff (Record prov) | r }
301− -> OmLayer (scope :: Scope) prov err
344+ -> OmLayer (scope :: Scope) err (Record prov)
302345 -> Aff (Record prov)
303346runScopedWith handlers layer = withScopedWith handlers layer pure
304347
@@ -309,7 +352,7 @@ withScopedWith
309352 => VariantMatchCases rl err_ (Aff (Record prov))
310353 => Union err_ () (exception :: Error | err)
311354 => { exception :: Error -> Aff (Record prov) | r }
312− -> OmLayer (scope :: Scope) prov err
355+ -> OmLayer (scope :: Scope) err (Record prov)
313356 -> (Record prov -> Aff a)
314357 -> Aff a
315358withScopedWith handlers layer callback = bracket acquire release use
@@ -340,9 +383,9 @@ combineRequirements
340383 => Nub provMergedRaw provMerged
341384 => Keys req1
342385 => Keys req2
343− => OmLayer req1 prov1 err1
344− -> OmLayer req2 prov2 err2
345− -> OmLayer reqDeduped provMerged errDeduped
386+ => OmLayer req1 err1 (Record prov1)
387+ -> OmLayer req2 err2 (Record prov2)
388+ -> OmLayer reqDeduped errDeduped (Record provMerged)
346389combineRequirements layer1 layer2 = OmLayer (freshId unit) do
347390 rec1 <- Om.expand (buildLayer layer1)
348391 rec2 <- Om.expand (buildLayer layer2)
@@ -366,9 +409,9 @@ provide
366409 => Keys req
367410 => Keys prov1
368411 => Keys req2
369− => OmLayer req2 prov2 err2
370− -> OmLayer req prov1 err1
371− -> OmLayer req prov2 errDeduped
412+ => OmLayer req2 err2 (Record prov2)
413+ -> OmLayer req err1 (Record prov1)
414+ -> OmLayer req errDeduped (Record prov2)
372415provide l2 l1 = OmLayer (freshId unit) do
373416 prov1 <- Om.expand (buildLayer l1)
374417 Om.expand (buildLayer l2) # Om.widenCtx prov1
test/Playground.purs+4−4
@@ -24,22 +24,22 @@ type Cache = { get :: String -> Effect (Maybe String) }
2424type CacheL r = (cache :: Cache | r)
2525type CacheL_ = CacheL ()
2626
27−loggerLayer :: OmLayer ConfigL_ LoggerL_ ()
27+loggerLayer :: OmLayer ConfigL_ () { logger :: Logger }
2828loggerLayer = makeLayer do
2929 { config } <- Om.ask
3030 pure { logger: { log: \msg -> pure unit } }
3131
32−databaseLayer :: OmLayer ConfigL_ DatabaseL_ ()
32+databaseLayer :: OmLayer ConfigL_ () { database :: Database }
3333databaseLayer = makeLayer do
3434 { config } <- Om.ask
3535 pure { database: { query: \q -> pure [] } }
3636
37−cacheLayer :: OmLayer (LoggerL + DatabaseL_) CacheL_ ()
37+cacheLayer :: OmLayer (LoggerL + DatabaseL_) () { cache :: Cache }
3838cacheLayer = makeLayer do
3939 { logger, database } <- Om.ask
4040 pure { cache: { get: \key -> pure Nothing } }
4141
42−programLayer :: OmLayer (DatabaseL + CacheL_) () ()
42+programLayer :: OmLayer (DatabaseL + CacheL_) () {}
4343programLayer = makeLayer do
4444 pure {}
4545
test/Test/Main.purs+102−45
@@ -2,6 +2,7 @@ module Test.Main where
22
33import Prelude
44
5+import Control.Parallel (parApply)
56import Effect (Effect)
67import Effect.Aff (launchAff_, throwError, try)
78import Effect.Aff.Class (liftAff)
@@ -29,12 +30,12 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
2930
3031 it "proves Row.Nub deduplicates shared requirements at type level" do
3132 let
32− loggerLayer :: OmLayer (config :: Config) (logger :: Logger) ()
33+ loggerLayer :: OmLayer (config :: Config) () { logger :: Logger }
3334 loggerLayer = makeLayer do
3435 { config } <- Om.ask
3536 pure { logger: { log: \msg -> Console.log $ "[" <> config.host <> "] " <> msg } }
3637
37− databaseLayer :: OmLayer (config :: Config) (database :: Database) ()
38+ databaseLayer :: OmLayer (config :: Config) () { database :: Database }
3839 databaseLayer = makeLayer do
3940 { config } <- Om.ask
4041 pure { database: { query: \q -> pure [ "Result from " <> config.host ] } }
@@ -49,7 +50,7 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
4950 it "proves deduplication at runtime via shared Ref" do
5051 accessLog <- liftEffect $ Ref.new ""
5152 let
52− combinedLayer :: OmLayer (config :: Config, accessLog :: Ref.Ref String) (logger :: Logger, database :: Database) ()
53+ combinedLayer :: OmLayer (config :: Config, accessLog :: Ref.Ref String) () { logger :: Logger, database :: Database }
5354 combinedLayer = makeLayer do
5455 { config, accessLog: log } <- Om.ask
5556 liftEffect $ Ref.modify_ (_ <> "config-accessed-by-logger ") log
@@ -79,22 +80,22 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
7980 it "runs finalizers in reverse order on success" do
8081 log <- liftEffect $ Ref.new []
8182 let
82− layerA :: OmLayer (scope :: Scope) (a :: String) ()
83+ layerA :: OmLayer (scope :: Scope) () { a :: String }
8384 layerA = makeScopedLayer
8485 (pure { a: "A" })
8586 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-A" ]) log)
8687
87− layerB :: OmLayer (scope :: Scope) (b :: String) ()
88+ layerB :: OmLayer (scope :: Scope) () { b :: String }
8889 layerB = makeScopedLayer
8990 (pure { b: "B" })
9091 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-B" ]) log)
9192
92− layerC :: OmLayer (scope :: Scope) (c :: String) ()
93+ layerC :: OmLayer (scope :: Scope) () { c :: String }
9394 layerC = makeScopedLayer
9495 (pure { c: "C" })
9596 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-C" ]) log)
9697
97− layerBC :: OmLayer (scope :: Scope) (b :: String, c :: String) ()
98+ layerBC :: OmLayer (scope :: Scope) () { b :: String, c :: String }
9899 layerBC = combineRequirements layerB layerC
99100
100101 combined = combineRequirements layerA layerBC
@@ -106,7 +107,7 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
106107 it "runs finalizers when the callback throws" do
107108 log <- liftEffect $ Ref.new []
108109 let
109− layer :: OmLayer (scope :: Scope) (value :: String) ()
110+ layer :: OmLayer (scope :: Scope) () { value :: String }
110111 layer = makeScopedLayer
111112 (pure { value: "acquired" })
112113 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "released" ]) log)
@@ -119,12 +120,12 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
119120 it "threads finalizers through vertical composition (provide)" do
120121 log <- liftEffect $ Ref.new []
121122 let
122− baseLayer :: OmLayer (scope :: Scope) (base :: String) ()
123+ baseLayer :: OmLayer (scope :: Scope) () { base :: String }
123124 baseLayer = makeScopedLayer
124125 (pure { base: "base-value" })
125126 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-base" ]) log)
126127
127− upperLayer :: OmLayer (scope :: Scope, base :: String) (upper :: String) ()
128+ upperLayer :: OmLayer (scope :: Scope, base :: String) () { upper :: String }
128129 upperLayer = makeScopedLayer
129130 ( do
130131 { base } <- Om.ask
@@ -158,23 +159,23 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
158159 it "fresh layer builds twice even when same base layer" do
159160 counter <- liftEffect $ Ref.new 0
160161 let
161− baseLayer :: OmLayer (scope :: Scope) (value :: String) ()
162+ baseLayer :: OmLayer (scope :: Scope) () { value :: String }
162163 baseLayer = makeLayer do
163164 liftEffect $ Ref.modify_ (_ + 1) counter
164165 pure { value: "built" }
165166
166− branch1 :: OmLayer (scope :: Scope) (out1 :: String) ()
167+ branch1 :: OmLayer (scope :: Scope) () { out1 :: String }
167168 branch1 = consumer `provide` fresh baseLayer
168169 where
169− consumer :: OmLayer (scope :: Scope, value :: String) (out1 :: String) ()
170+ consumer :: OmLayer (scope :: Scope, value :: String) () { out1 :: String }
170171 consumer = makeLayer do
171172 { value } <- Om.ask
172173 pure { out1: value <> "-1" }
173174
174− branch2 :: OmLayer (scope :: Scope) (out2 :: String) ()
175+ branch2 :: OmLayer (scope :: Scope) () { out2 :: String }
175176 branch2 = consumer `provide` fresh baseLayer
176177 where
177− consumer :: OmLayer (scope :: Scope, value :: String) (out2 :: String) ()
178+ consumer :: OmLayer (scope :: Scope, value :: String) () { out2 :: String }
178179 consumer = makeLayer do
179180 { value } <- Om.ask
180181 pure { out2: value <> "-2" }
@@ -190,12 +191,12 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
190191 it "combines scoped and non-scoped layers" do
191192 log <- liftEffect $ Ref.new []
192193 let
193− scopedLayer :: OmLayer (scope :: Scope) (scoped :: String) ()
194+ scopedLayer :: OmLayer (scope :: Scope) () { scoped :: String }
194195 scopedLayer = makeScopedLayer
195196 (pure { scoped: "yes" })
196197 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-scoped" ]) log)
197198
198− plainLayer :: OmLayer () (plain :: String) ()
199+ plainLayer :: OmLayer () () { plain :: String }
199200 plainLayer = makeLayer (pure { plain: "no-finalizer" })
200201
201202 combined = combineRequirements scopedLayer plainLayer
@@ -211,23 +212,23 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
211212 it "builds the same layer only once when used in two branches" do
212213 counter <- liftEffect $ Ref.new 0
213214 let
214− expensiveLayer :: OmLayer (scope :: Scope) (value :: String) ()
215+ expensiveLayer :: OmLayer (scope :: Scope) () { value :: String }
215216 expensiveLayer = makeLayer do
216217 liftEffect $ Ref.modify_ (_ + 1) counter
217218 pure { value: "built" }
218219
219− branch1 :: OmLayer (scope :: Scope) (out1 :: String) ()
220+ branch1 :: OmLayer (scope :: Scope) () { out1 :: String }
220221 branch1 = consumer `provide` expensiveLayer
221222 where
222− consumer :: OmLayer (scope :: Scope, value :: String) (out1 :: String) ()
223+ consumer :: OmLayer (scope :: Scope, value :: String) () { out1 :: String }
223224 consumer = makeLayer do
224225 { value } <- Om.ask
225226 pure { out1: value <> "-1" }
226227
227− branch2 :: OmLayer (scope :: Scope) (out2 :: String) ()
228+ branch2 :: OmLayer (scope :: Scope) () { out2 :: String }
228229 branch2 = consumer `provide` expensiveLayer
229230 where
230− consumer :: OmLayer (scope :: Scope, value :: String) (out2 :: String) ()
231+ consumer :: OmLayer (scope :: Scope, value :: String) () { out2 :: String }
231232 consumer = makeLayer do
232233 { value } <- Om.ask
233234 pure { out2: value <> "-2" }
@@ -243,23 +244,23 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
243244 it "memoized scoped layer runs finalizer only once" do
244245 log <- liftEffect $ Ref.new []
245246 let
246− dbLayer :: OmLayer (scope :: Scope) (db :: String) ()
247+ dbLayer :: OmLayer (scope :: Scope) () { db :: String }
247248 dbLayer = makeScopedLayer
248249 (pure { db: "connected" })
249250 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "db-closed" ]) log)
250251
251− repoBranch :: OmLayer (scope :: Scope) (repo :: String) ()
252+ repoBranch :: OmLayer (scope :: Scope) () { repo :: String }
252253 repoBranch = repo `provide` dbLayer
253254 where
254− repo :: OmLayer (scope :: Scope, db :: String) (repo :: String) ()
255+ repo :: OmLayer (scope :: Scope, db :: String) () { repo :: String }
255256 repo = makeLayer do
256257 { db } <- Om.ask
257258 pure { repo: db <> "-repo" }
258259
259− analyticsBranch :: OmLayer (scope :: Scope) (analytics :: String) ()
260+ analyticsBranch :: OmLayer (scope :: Scope) () { analytics :: String }
260261 analyticsBranch = analytics `provide` dbLayer
261262 where
262− analytics :: OmLayer (scope :: Scope, db :: String) (analytics :: String) ()
263+ analytics :: OmLayer (scope :: Scope, db :: String) () { analytics :: String }
263264 analytics = makeLayer do
264265 { db } <- Om.ask
265266 pure { analytics: db <> "-analytics" }
@@ -275,7 +276,7 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
275276 it "memoized bracketLayer acquires and releases only once" do
276277 log <- liftEffect $ Ref.new []
277278 let
278− connLayer :: OmLayer (scope :: Scope) (conn :: Int) ()
279+ connLayer :: OmLayer (scope :: Scope) () { conn :: Int }
279280 connLayer = bracketLayer
280281 ( do
281282 liftEffect $ Ref.modify_ (_ <> [ "acquire" ]) log
@@ -284,18 +285,18 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
284285 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release" ]) log)
285286 (\n -> pure { conn: n })
286287
287− branch1 :: OmLayer (scope :: Scope) (x :: String) ()
288+ branch1 :: OmLayer (scope :: Scope) () { x :: String }
288289 branch1 = upper `provide` connLayer
289290 where
290− upper :: OmLayer (scope :: Scope, conn :: Int) (x :: String) ()
291+ upper :: OmLayer (scope :: Scope, conn :: Int) () { x :: String }
291292 upper = makeLayer do
292293 { conn } <- Om.ask
293294 pure { x: show conn }
294295
295− branch2 :: OmLayer (scope :: Scope) (y :: String) ()
296+ branch2 :: OmLayer (scope :: Scope) () { y :: String }
296297 branch2 = upper `provide` connLayer
297298 where
298− upper :: OmLayer (scope :: Scope, conn :: Int) (y :: String) ()
299+ upper :: OmLayer (scope :: Scope, conn :: Int) () { y :: String }
299300 upper = makeLayer do
300301 { conn } <- Om.ask
301302 pure { y: show (conn + 1) }
@@ -312,15 +313,15 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
312313
313314 it "composes layers with different error types" do
314315 let
315− dbLayer :: OmLayer () (db :: String) (dbError :: String)
316+ dbLayer :: OmLayer () (dbError :: String) { db :: String }
316317 dbLayer = makeLayer do
317318 pure { db: "connected" }
318319
319− cacheLayer :: OmLayer () (cache :: String) (cacheError :: String)
320+ cacheLayer :: OmLayer () (cacheError :: String) { cache :: String }
320321 cacheLayer = makeLayer do
321322 pure { cache: "ready" }
322323
323− combined :: OmLayer () (db :: String, cache :: String) (dbError :: String, cacheError :: String)
324+ combined :: OmLayer () (dbError :: String, cacheError :: String) { db :: String, cache :: String }
324325 combined = combineRequirements dbLayer cacheLayer
325326
326327 result <- Om.runOm {}
@@ -334,11 +335,11 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
334335
335336 it "propagates errors from the first layer" do
336337 let
337− failingLayer :: OmLayer () (db :: String) (dbError :: String)
338+ failingLayer :: OmLayer () (dbError :: String) { db :: String }
338339 failingLayer = makeLayer do
339340 Om.throw { dbError: "connection refused" }
340341
341− okLayer :: OmLayer () (cache :: String) ()
342+ okLayer :: OmLayer () () { cache :: String }
342343 okLayer = makeLayer do
343344 pure { cache: "ready" }
344345
@@ -358,11 +359,11 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
358359
359360 it "propagates errors from the second layer" do
360361 let
361− okLayer :: OmLayer () (db :: String) ()
362+ okLayer :: OmLayer () () { db :: String }
362363 okLayer = makeLayer do
363364 pure { db: "connected" }
364365
365− failingLayer :: OmLayer () (cache :: String) (cacheError :: String)
366+ failingLayer :: OmLayer () (cacheError :: String) { cache :: String }
366367 failingLayer = makeLayer do
367368 Om.throw { cacheError: "cache unavailable" }
368369
@@ -382,11 +383,11 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
382383
383384 it "propagates errors through vertical composition" do
384385 let
385− baseLayer :: OmLayer () (base :: String) (baseError :: String)
386+ baseLayer :: OmLayer () (baseError :: String) { base :: String }
386387 baseLayer = makeLayer do
387388 Om.throw { baseError: "base failed" }
388389
389− upperLayer :: OmLayer (base :: String) (upper :: String) (upperError :: String)
390+ upperLayer :: OmLayer (base :: String) (upperError :: String) { upper :: String }
390391 upperLayer = makeLayer do
391392 { base } <- Om.ask
392393 pure { upper: base <> "-extended" }
@@ -409,12 +410,12 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
409410 it "scoped layers with errors run via runScopedWith" do
410411 log <- liftEffect $ Ref.new []
411412 let
412− dbLayer :: OmLayer (scope :: Scope) (db :: String) (dbError :: String)
413+ dbLayer :: OmLayer (scope :: Scope) (dbError :: String) { db :: String }
413414 dbLayer = makeScopedLayer
414415 (pure { db: "connected" })
415416 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "db-closed" ]) log)
416417
417− cacheLayer :: OmLayer (scope :: Scope) (cache :: String) (cacheError :: String)
418+ cacheLayer :: OmLayer (scope :: Scope) (cacheError :: String) { cache :: String }
418419 cacheLayer = makeScopedLayer
419420 (pure { cache: "ready" })
420421 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "cache-closed" ]) log)
@@ -436,12 +437,12 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
436437 it "scoped layer error triggers finalizers" do
437438 log <- liftEffect $ Ref.new []
438439 let
439− goodLayer :: OmLayer (scope :: Scope) (good :: String) ()
440+ goodLayer :: OmLayer (scope :: Scope) () { good :: String }
440441 goodLayer = makeScopedLayer
441442 (pure { good: "ok" })
442443 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "good-closed" ]) log)
443444
444− failLayer :: OmLayer (scope :: Scope) (bad :: String) (buildError :: String)
445+ failLayer :: OmLayer (scope :: Scope) (buildError :: String) { bad :: String }
445446 failLayer = makeScopedLayer
446447 (Om.throw { buildError: "build failed" })
447448 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "bad-closed" ]) log)
@@ -459,3 +460,59 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
459460 Left _ -> pure unit
460461 finalLog <- liftEffect $ Ref.read log
461462 finalLog `shouldEqual` [ "good-closed" ]
463+
464+ describe "Functor / Apply / Bind" do
465+
466+ it "maps over layer output" do
467+ let
468+ layer :: OmLayer () () { value :: Int }
469+ layer = makeLayer (pure { value: 10 })
470+
471+ mapped = layer <#> \r -> { doubled: r.value * 2 }
472+
473+ result <- Om.runOm {} { exception: \_ -> pure { doubled: 0 } } (runLayer {} mapped)
474+ result.doubled `shouldEqual` 20
475+
476+ it "applies two layers" do
477+ let
478+ fLayer :: OmLayer () () ({ a :: Int } -> { a :: Int, b :: String })
479+ fLayer = makeLayer (pure \r -> { a: r.a, b: show r.a })
480+
481+ aLayer :: OmLayer () () { a :: Int }
482+ aLayer = makeLayer (pure { a: 42 })
483+
484+ result <- Om.runOm {} { exception: \_ -> pure { a: 0, b: "" } } (runLayer {} (fLayer <*> aLayer))
485+ result.a `shouldEqual` 42
486+ result.b `shouldEqual` "42"
487+
488+ it "binds layers sequentially" do
489+ let
490+ layer = do
491+ r <- makeLayer (pure { x: 5 }) :: OmLayer () () { x :: Int }
492+ makeLayer (pure { result: r.x + 10 })
493+
494+ result <- Om.runOm {} { exception: \_ -> pure { result: 0 } } (runLayer {} layer)
495+ result.result `shouldEqual` 15
496+
497+ describe "Parallel" do
498+
499+ it "runs layers in parallel via parApply" do
500+ log <- liftEffect $ Ref.new []
501+ let
502+ layerA :: OmLayer () () { a :: String }
503+ layerA = makeLayer do
504+ liftEffect $ Ref.modify_ (_ <> [ "a" ]) log
505+ pure { a: "A" }
506+
507+ layerB :: OmLayer () () { b :: String }
508+ layerB = makeLayer do
509+ liftEffect $ Ref.modify_ (_ <> [ "b" ]) log
510+ pure { b: "B" }
511+
512+ combined = parApply (map (\ra rb -> { a: ra.a, b: rb.b }) layerA) layerB
513+
514+ result <- Om.runOm {} { exception: \_ -> pure { a: "", b: "" } } (runLayer {} combined)
515+ result.a `shouldEqual` "A"
516+ result.b `shouldEqual` "B"
517+ finalLog <- liftEffect $ Ref.read log
518+ finalLog `shouldEqual` [ "a", "b" ]