git.iko.soy/rowtype-yoga/purescript-yoga-om-layer
on main help sign in
Add memoized bracketLayer test
commitc8076e3dde78026790700c09e441c5f00f309782 authorMark <mark.eibes@gmail.com> dateFri 13 Feb 2026, 21:49 (8 months ago) parentacadf1c
6 files changed+154−235
spago.lock+19 spago.yaml+3 src/Yoga/Om/Layer.js+4 src/Yoga/Om/Layer.purs+81−95 test/Playground.purs+6−43 test/Test/Main.purs+41−97
spago.lock+19
@@ -11,12 +11,21 @@
1111 {
1212 "arrays": ">=7.3.0 <8.0.0"
1313 },
14+ {
15+ "effect": ">=4.0.0 <5.0.0"
16+ },
1417 {
1518 "foldable-traversable": ">=6.0.0 <7.0.0"
1619 },
20+ {
21+ "integers": ">=6.0.0 <7.0.0"
22+ },
1723 {
1824 "maybe": ">=6.0.0 <7.0.0"
1925 },
26+ {
27+ "nullable": ">=6.0.0 <7.0.0"
28+ },
2029 {
2130 "ordered-collections": ">=3.2.0 <4.0.0"
2231 },
@@ -508,6 +517,16 @@
508517 "prelude"
509518 ]
510519 },
520+ "nullable": {
521+ "type": "registry",
522+ "version": "6.0.0",
523+ "integrity": "sha256-CDAZk+L9Sc0GCFTkE8n+qdp4Ys8avvnkU+m2hVALt7M=",
524+ "dependencies": [
525+ "functions",
526+ "maybe",
527+ "prelude"
528+ ]
529+ },
511530 "numbers": {
512531 "type": "registry",
513532 "version": "9.0.1",
spago.yaml+3
@@ -11,8 +11,11 @@ package:
1111 dependencies:
1212 - aff: ">=7.1.0 <8.0.0"
1313 - arrays: ">=7.3.0 <8.0.0"
14+ - effect: ">=4.0.0 <5.0.0"
1415 - foldable-traversable: ">=6.0.0 <7.0.0"
16+ - integers: ">=6.0.0 <7.0.0"
1517 - maybe: ">=6.0.0 <7.0.0"
18+ - nullable: ">=6.0.0 <7.0.0"
1619 - ordered-collections: ">=3.2.0 <4.0.0"
1720 - prelude: ">=6.0.2 <7.0.0"
1821 - record: ">=4.0.0 <5.0.0"
src/Yoga/Om/Layer.jsnew file+4
@@ -0,0 +1,4 @@
1+let counter = 0;
2+export const nextId = () => ++counter;
3+
4+export const tryGetScope = (ctx) => ctx.scope || null;
src/Yoga/Om/Layer.purs+81−95
@@ -1,4 +1,3 @@
1−-- @inline export makeLayer arity=1
21-- @inline export runLayer arity=2
32module Yoga.Om.Layer
43 ( OmLayer
@@ -7,16 +6,13 @@ module Yoga.Om.Layer
76 , makeScopedLayer
87 , bracketLayer
98 , acquireRelease
10− , memoized
9+ , fresh
1110 , runLayer
1211 , runScoped
1312 , withScoped
1413 , combineRequirements
1514 , provide
1615 , (>->)
17− , class CheckMemoKeys
18− , class CheckMemoKeysList
19− , class CheckKeyNotDuplicated
2016 , class CheckAllProvided
2117 , class CheckAllLabelsExist
2218 , class CheckLabelExists
@@ -29,19 +25,19 @@ import Data.Foldable (for_)
2925import Data.Map (Map)
3026import Data.Map as Map
3127import Data.Maybe (Maybe(..))
32−import Data.Symbol (class IsSymbol, reflectSymbol)
28+import Data.Nullable (Nullable, toMaybe)
3329import Effect (Effect)
3430import Effect.Aff (Aff, bracket)
3531import Effect.Class (liftEffect)
3632import Effect.Ref (Ref)
3733import Effect.Ref as Ref
34+import Effect.Unsafe (unsafePerformEffect)
3835import Prim.Row (class Nub, class Union)
3936import Prim.RowList (class RowToList, Cons, Nil, RowList)
4037import Prim.TypeError (class Fail, Above, Quote, Text)
4138import Record as Record
4239import Record.Studio (class Keys)
4340import Type.Equality (class TypeEquals)
44−import Type.Proxy (Proxy(..))
4541import Unsafe.Coerce (unsafeCoerce)
4642import Yoga.Om (Om)
4743import Yoga.Om as Om
@@ -52,10 +48,11 @@ import Yoga.Om as Om
5248
5349-- | A first-class representation of resource lifetime and memoization.
5450-- | Finalizers are registered via `acquireRelease` and run in reverse order
55−-- | when the scope is closed. Memoized layers share results via the cache.
51+-- | when the scope is closed. Layers are automatically memoized by identity
52+-- | within a scope — the same layer value builds only once.
5653newtype Scope = Scope
5754 { finalizers :: Ref (Array (Aff Unit))
58− , cache :: Ref (Map String Dynamic)
55+ , cache :: Ref (Map Int Dynamic)
5956 }
6057
6158-- | Opaque box for cached values, retrieved via `unsafeCoerce`.
@@ -84,21 +81,61 @@ closeScope (Scope s) = do
8481 fins <- liftEffect $ Ref.read s.finalizers
8582 for_ (Array.reverse fins) identity
8683
84+-- =============================================================================
85+-- Unique layer IDs
86+-- =============================================================================
87+
88+foreign import nextId :: Effect Int
89+foreign import tryGetScope :: forall r. Record r -> Nullable Scope
90+
91+-- | Generate a unique layer ID. Uses unsafePerformEffect since layer
92+-- | construction is at module init time (like Scala's `object` identity).
93+-- | This is a function (Unit -> Int) to ensure each call site gets a unique ID.
94+freshId :: Unit -> Int
95+freshId _ = unsafePerformEffect nextId
96+
8797-- =============================================================================
8898-- OmLayer
8999-- =============================================================================
90100
91−-- | A layer that requires dependencies (req), provides services (prov),
92−-- | and tracks memoization keys (memo) to prevent key collisions at compile time.
93−data OmLayer req prov err (memo :: Row Type) = OmLayer (Om (Record req) err (Record prov))
101+-- | A layer that requires dependencies (req) and provides services (prov).
102+-- | Each layer has a unique identity used for automatic memoization within a Scope.
103+data OmLayer req prov err = OmLayer Int (Om (Record req) err (Record prov))
104+
105+-- =============================================================================
106+-- Automatic memoization
107+-- =============================================================================
108+
109+-- | Wrap an Om computation with cache logic.
110+-- | If a scope is available in the context, checks/populates the cache by layer ID.
111+-- | If no scope is present, runs the computation directly.
112+withCache :: forall req prov err. Int -> Om (Record req) err (Record prov) -> Om (Record req) err (Record prov)
113+withCache layerId om = do
114+ ctx <- Om.ask
115+ case toMaybe (tryGetScope ctx) of
116+ Nothing -> om
117+ Just (Scope s) -> do
118+ cached <- liftEffect $ Map.lookup layerId <$> Ref.read s.cache
119+ case cached of
120+ Just hit -> pure (fromDynamic hit)
121+ Nothing -> do
122+ prov <- om
123+ liftEffect $ Ref.modify_ (Map.insert layerId (toDynamic prov)) s.cache
124+ pure prov
94125
95126-- =============================================================================
96127-- Layer constructors
97128-- =============================================================================
98129
99130-- | Create a layer from an Om computation.
100−makeLayer :: forall req prov err. Om (Record req) err (Record prov) -> OmLayer req prov err ()
101−makeLayer = OmLayer
131+-- | The layer is automatically memoized within a Scope — the same layer value
132+-- | used in multiple branches of a dependency graph builds only once.
133+makeLayer :: forall req prov err. Om (Record req) err (Record prov) -> OmLayer req prov err
134+makeLayer om = OmLayer (freshId unit) om
135+
136+-- | Extract the Om from a layer, wrapped with cache logic.
137+buildLayer :: forall req prov err. OmLayer req prov err -> Om (Record req) err (Record prov)
138+buildLayer (OmLayer layerId om) = withCache layerId om
102139
103140-- | Create a scoped layer with acquire/release semantics.
104141-- | The release function runs when the enclosing scope closes.
@@ -107,7 +144,7 @@ makeScopedLayer
107144 :: forall req prov err
108145 . Om { scope :: Scope | req } err (Record prov)
109146 -> (Record prov -> Aff Unit)
110− -> OmLayer (scope :: Scope | req) prov err ()
147+ -> OmLayer (scope :: Scope | req) prov err
111148makeScopedLayer acquire release = makeLayer do
112149 acquireRelease acquire release
113150
@@ -118,7 +155,7 @@ bracketLayer
118155 . Om { scope :: Scope | req } err resource
119156 -> (resource -> Aff Unit)
120157 -> (resource -> Om { scope :: Scope | req } err (Record prov))
121− -> OmLayer (scope :: Scope | req) prov err ()
158+ -> OmLayer (scope :: Scope | req) prov err
122159bracketLayer acquire release use = makeLayer do
123160 resource <- acquireRelease acquire (\r -> release r)
124161 use resource
@@ -137,57 +174,11 @@ acquireRelease acquire release = do
137174 addFinalizer scope (release a) # liftEffect
138175 pure a
139176
140−-- | Memoize a layer by a compile-time symbol key.
141−-- | If the same key has already been built in this scope, the cached result
142−-- | is returned without re-running the layer.
143−memoized
144− :: forall sym req prov err memo
145− . IsSymbol sym
146− => Proxy sym
147− -> OmLayer (scope :: Scope | req) prov err memo
148− -> OmLayer (scope :: Scope | req) prov err (sym :: Record prov | memo)
149−memoized _ (OmLayer om) = OmLayer do
150− { scope: Scope s } <- Om.ask
151− let key = reflectSymbol (Proxy :: Proxy sym)
152− cached <- liftEffect $ Map.lookup key <$> Ref.read s.cache
153− case cached of
154− Just hit -> pure (fromDynamic hit)
155− Nothing -> do
156− prov <- om
157− liftEffect $ Ref.modify_ (Map.insert key (toDynamic prov)) s.cache
158− pure prov
159−
160−-- =============================================================================
161−-- Memo key safety — reject duplicate keys with different types
162−-- =============================================================================
163−
164−class CheckMemoKeys (row :: Row Type)
165−
166−instance (RowToList row rl, CheckMemoKeysList rl) => CheckMemoKeys row
167−
168−class CheckMemoKeysList (rl :: RowList Type)
169−
170−instance CheckMemoKeysList Nil
171−
172−instance
173− ( CheckMemoKeysList tail
174− , CheckKeyNotDuplicated label ty tail
175− ) =>
176− CheckMemoKeysList (Cons label ty tail)
177−
178−class CheckKeyNotDuplicated (label :: Symbol) (ty :: Type) (rest :: RowList Type)
179−
180−instance CheckKeyNotDuplicated label ty Nil
181−
182−instance
183− ( TypeEquals ty ty'
184− , CheckKeyNotDuplicated label ty tail
185− ) =>
186− CheckKeyNotDuplicated label ty (Cons label ty' tail)
187−
188−else instance
189− CheckKeyNotDuplicated label ty tail =>
190− CheckKeyNotDuplicated label ty (Cons otherLabel otherTy tail)
177+-- | Create a fresh (non-memoized) copy of a layer.
178+-- | The new layer has a unique identity and will always build independently,
179+-- | even within the same scope.
180+fresh :: forall req prov err. OmLayer req prov err -> OmLayer req prov err
181+fresh (OmLayer _ om) = OmLayer (freshId unit) om
191182
192183-- =============================================================================
193184-- Custom Type Errors for Missing Dependencies
@@ -265,12 +256,12 @@ else instance
265256
266257-- | Run a layer with a given context, with custom error if requirements aren't met.
267258runLayer
268− :: forall req prov err available memo
259+ :: forall req prov err available
269260 . CheckAllProvided req available
270261 => Record available
271− -> OmLayer req prov err memo
262+ -> OmLayer req prov err
272263 -> Om (Record available) err (Record prov)
273−runLayer _ctx (OmLayer om) = widenCtx om
264+runLayer _ctx layer = widenCtx (buildLayer layer)
274265 where
275266 widenCtx :: Om (Record req) err (Record prov) -> Om (Record available) err (Record prov)
276267 widenCtx = unsafeCoerce
@@ -278,8 +269,8 @@ runLayer _ctx (OmLayer om) = widenCtx om
278269-- | Build a fully-provided scoped layer, return the provisions.
279270-- | All finalizers run after the provisions are returned.
280271runScoped
281− :: forall prov memo
282− . OmLayer (scope :: Scope) prov () memo
272+ :: forall prov
273+ . OmLayer (scope :: Scope) prov ()
283274 -> Aff (Record prov)
284275runScoped layer = withScoped layer pure
285276
@@ -288,14 +279,15 @@ runScoped layer = withScoped layer pure
288279-- | running all finalizers in reverse order — whether by success, failure,
289280-- | or interruption.
290281withScoped
291− :: forall prov a memo
292− . OmLayer (scope :: Scope) prov () memo
282+ :: forall prov a
283+ . OmLayer (scope :: Scope) prov ()
293284 -> (Record prov -> Aff a)
294285 -> Aff a
295−withScoped (OmLayer om) callback = bracket acquire release use
286+withScoped layer callback = bracket acquire release use
296287 where
297288 acquire = do
298289 scope <- newScope # liftEffect
290+ let om = buildLayer layer
299291 prov <- Om.runOm { scope } { exception: \_ -> pure (unsafeCoerce {}) } om
300292 pure { scope, prov }
301293 release { scope } = closeScope scope
@@ -306,7 +298,7 @@ withScoped (OmLayer om) callback = bracket acquire release use
306298-- =============================================================================
307299
308300combineRequirements
309− :: forall req1 req2 prov1 prov2 err1 err2 memo1 memo2 provMerged provMergedRaw reqMerged reqDeduped memoMerged memoDeduped _req1 _req2 _err1 _err2
301+ :: forall req1 req2 prov1 prov2 err1 err2 provMerged provMergedRaw reqMerged reqDeduped _req1 _req2 _err1 _err2
310302 . Union req1 req2 reqMerged
311303 => Nub reqMerged reqDeduped
312304 => Union req1 _req1 reqDeduped
@@ -315,17 +307,14 @@ combineRequirements
315307 => Union err2 _err2 ()
316308 => Union prov1 prov2 provMergedRaw
317309 => Nub provMergedRaw provMerged
318− => Union memo1 memo2 memoMerged
319− => CheckMemoKeys memoMerged
320− => Nub memoMerged memoDeduped
321310 => Keys req1
322311 => Keys req2
323− => OmLayer req1 prov1 err1 memo1
324− -> OmLayer req2 prov2 err2 memo2
325− -> OmLayer reqDeduped provMerged () memoDeduped
326−combineRequirements (OmLayer build1) (OmLayer build2) = OmLayer do
327− rec1 <- Om.expand build1
328− rec2 <- Om.expand build2
312+ => OmLayer req1 prov1 err1
313+ -> OmLayer req2 prov2 err2
314+ -> OmLayer reqDeduped provMerged ()
315+combineRequirements layer1 layer2 = OmLayer (freshId unit) do
316+ rec1 <- Om.expand (buildLayer layer1)
317+ rec2 <- Om.expand (buildLayer layer2)
329318 pure (Record.merge rec1 rec2)
330319
331320-- =============================================================================
@@ -333,7 +322,7 @@ combineRequirements (OmLayer build1) (OmLayer build2) = OmLayer do
333322-- =============================================================================
334323
335324provide
336− :: forall req prov1 prov2 err1 err2 req2 reqOut memo1 memo2 memoMerged memoDeduped _req _prov1 _err1 _err2
325+ :: forall req prov1 prov2 err1 err2 req2 reqOut _req _prov1 _err1 _err2
337326 . Union req _req req
338327 => Union prov1 _prov1 prov1
339328 => Union err1 _err1 ()
@@ -341,17 +330,14 @@ provide
341330 => Union prov1 req reqOut
342331 => Nub reqOut reqOut
343332 => Union req2 _prov1 reqOut
344− => Union memo1 memo2 memoMerged
345− => CheckMemoKeys memoMerged
346− => Nub memoMerged memoDeduped
347333 => Keys req
348334 => Keys prov1
349335 => Keys req2
350− => OmLayer req2 prov2 err2 memo2
351− -> OmLayer req prov1 err1 memo1
352− -> OmLayer req prov2 () memoDeduped
353−provide (OmLayer layer2) (OmLayer layer1) = OmLayer do
354− prov1 <- Om.expand layer1
355− Om.expand layer2 # Om.widenCtx prov1
336+ => OmLayer req2 prov2 err2
337+ -> OmLayer req prov1 err1
338+ -> OmLayer req prov2 ()
339+provide l2 l1 = OmLayer (freshId unit) do
340+ prov1 <- Om.expand (buildLayer l1)
341+ Om.expand (buildLayer l2) # Om.widenCtx prov1
356342
357343infixl 9 provide as >->
test/Playground.purs+6−43
@@ -6,9 +6,9 @@ import Data.Maybe (Maybe(..))
66import Effect (Effect)
77import Effect.Console as Console
88import Type.Row (type (+))
9−import Yoga.Om (Om, launchOm_)
9+import Yoga.Om (launchOm_)
1010import Yoga.Om as Om
11−import Yoga.Om.Layer (OmLayer, makeLayer, runLayer, combineRequirements)
11+import Yoga.Om.Layer (OmLayer, makeLayer, runLayer)
1212
1313-- Define some example types
1414type Config = { port :: Int, host :: String }
@@ -24,25 +24,22 @@ type Cache = { get :: String -> Effect (Maybe String) }
2424type CacheL r = (cache :: Cache | r)
2525type CacheL_ = CacheL ()
2626
27−-- Example layer 1: needs config, provides logger
28−loggerLayer :: OmLayer ConfigL_ LoggerL_ () _
27+loggerLayer :: OmLayer ConfigL_ LoggerL_ ()
2928loggerLayer = makeLayer do
3029 { config } <- Om.ask
3130 pure { logger: { log: \msg -> pure unit } }
3231
33−-- Example layer 2: needs config, provides database
34−databaseLayer :: OmLayer ConfigL_ DatabaseL_ () _
32+databaseLayer :: OmLayer ConfigL_ DatabaseL_ ()
3533databaseLayer = makeLayer do
3634 { config } <- Om.ask
3735 pure { database: { query: \q -> pure [] } }
3836
39−-- Example layer 3: needs logger and database, provides cache
40−cacheLayer :: OmLayer (LoggerL + DatabaseL_) CacheL_ () _
37+cacheLayer :: OmLayer (LoggerL + DatabaseL_) CacheL_ ()
4138cacheLayer = makeLayer do
4239 { logger, database } <- Om.ask
4340 pure { cache: { get: \key -> pure Nothing } }
4441
45−programLayer :: OmLayer (DatabaseL + CacheL_) () () _
42+programLayer :: OmLayer (DatabaseL + CacheL_) () ()
4643programLayer = makeLayer do
4744 pure {}
4845
@@ -53,37 +50,3 @@ aProgram = launchOm_ do
5350 , database: { query: \q -> pure [] }
5451 , cache: { get: \key -> pure Nothing }
5552 }
56−
57−--------------------------------------------------------------------------------
58−-- TRY THESE! Uncomment one at a time to see different error messages
59−--------------------------------------------------------------------------------
60−
61−-- ❌ ERROR 1: Missing config entirely
62−-- Uncomment this to see the error:
63−-- example1 :: Om (Record ()) () (Record (logger :: Logger))
64−-- example1 = runLayer {} loggerLayer
65−
66−-- ❌ ERROR 2: Partial context - have logger, missing database
67−-- Uncomment this to see the error:
68−-- example2 :: Om (Record (logger :: Logger)) () (Record (cache :: Cache))
69−-- example2 = runLayer { logger: { log: \_ -> pure unit } } cacheLayer
70−
71−-- ❌ ERROR 3: Multiple missing dependencies
72−-- Uncomment this to see the error:
73−-- combined :: OmLayer (config :: Config) (logger :: Logger, database :: Database) ()
74−-- combined = combineRequirements loggerLayer databaseLayer
75−-- example3 :: Om (Record ()) () (Record (logger :: Logger, database :: Database))
76−-- example3 = runLayer {} combined
77−
78−-- ✅ SUCCESS: All dependencies provided!
79−-- This one works! Uncomment to verify:
80−-- example4 :: Om (Record (config :: Config)) () (Record (logger :: Logger))
81−-- example4 = runLayer { config: { port: 5432, host: "localhost" } } loggerLayer
82−
83−--------------------------------------------------------------------------------
84−-- Instructions:
85−-- 1. Uncomment ONE of the examples above
86−-- 2. Run: npx spago build -p yoga-om-layer
87−-- 3. See the beautiful error message!
88−-- 4. Try different combinations to explore
89−--------------------------------------------------------------------------------
test/Test/Main.purs+41−97
@@ -14,8 +14,7 @@ import Test.Spec.Assertions (shouldEqual)
1414import Test.Spec.Reporter (consoleReporter)
1515import Test.Spec.Runner (runSpec)
1616import Yoga.Om as Om
17−import Type.Proxy (Proxy(..))
18−import Yoga.Om.Layer (OmLayer, Scope, makeLayer, makeScopedLayer, bracketLayer, memoized, combineRequirements, runLayer, runScoped, withScoped, provide)
17+import Yoga.Om.Layer (OmLayer, Scope, makeLayer, makeScopedLayer, bracketLayer, combineRequirements, runLayer, runScoped, withScoped, provide)
1918
2019-- Example types
2120type Config = { port :: Int, host :: String }
@@ -27,71 +26,34 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
2726 describe "OmLayer - Proof of Concept: Type-Level Deduplication" do
2827
2928 it "proves Row.Nub deduplicates shared requirements at type level" do
30− -- Define layers with dependencies
3129 let
32− -- Layer that requires config
33− loggerLayer :: OmLayer (config :: Config) (logger :: Logger) () _
30+ loggerLayer :: OmLayer (config :: Config) (logger :: Logger) ()
3431 loggerLayer = makeLayer do
3532 { config } <- Om.ask
3633 pure { logger: { log: \msg -> Console.log $ "[" <> config.host <> "] " <> msg } }
3734
38− -- Layer that also requires config
39− databaseLayer :: OmLayer (config :: Config) (database :: Database) () _
35+ databaseLayer :: OmLayer (config :: Config) (database :: Database) ()
4036 databaseLayer = makeLayer do
4137 { config } <- Om.ask
4238 pure { database: { query: \q -> pure [ "Result from " <> config.host ] } }
4339
44− -- The proof: This type-checks!
45− -- combineRequirements takes two layers that each need (config :: Config)
46− -- and produces a layer that needs (config :: Config) - NOT (config, config)!
4740 let
48− -- The type annotation proves deduplication works:
49− _proofOfDeduplication
50− :: OmLayer (config :: Config) _ _ _
41+ _proofOfDeduplication :: OmLayer (config :: Config) _ _
5142 _proofOfDeduplication = combineRequirements loggerLayer databaseLayer
5243
53− Console.log "✓ PROOF OF CONCEPT SUCCESS!"
54− Console.log " "
55− Console.log " Key insight: Row.Nub automatically deduplicates at the type level"
56− Console.log " "
57− Console.log " Given:"
58− Console.log " layer1 :: OmLayer (config :: Config) (logger :: Logger) ()"
59− Console.log " layer2 :: OmLayer (config :: Config) (database :: Database) ()"
60− Console.log " "
61− Console.log " combineRequirements layer1 layer2 has type:"
62− Console.log " OmLayer (config :: Config) _ _"
63− Console.log " "
64− Console.log " NOT OmLayer (config :: Config, config :: Config) _ _"
65− Console.log " "
66− Console.log " The (config, config) requirement is automatically deduplicated"
67− Console.log " to just (config) by the Nub constraint in combineRequirements!"
68− Console.log " "
69− Console.log " This is the foundation for ZLayer-style dependency injection"
70− Console.log " where the type system automatically handles shared dependencies."
71−
44+ Console.log "Type-level deduplication works: (config, config) -> (config)"
7245 pure unit
7346
7447 it "proves deduplication at runtime via shared Ref" do
75− -- Create a mutable reference to track accesses
7648 accessLog <- liftEffect $ Ref.new ""
77−
7849 let
79− -- Manually construct a combined layer that shows both "components"
80− -- accessing the same shared context (with deduplicated requirements)
81− combinedLayer :: OmLayer (config :: Config, accessLog :: Ref.Ref String) (logger :: Logger, database :: Database) () _
50+ combinedLayer :: OmLayer (config :: Config, accessLog :: Ref.Ref String) (logger :: Logger, database :: Database) ()
8251 combinedLayer = makeLayer do
83− -- Get the shared context - note the type signature shows it's deduplicated!
8452 { config, accessLog: log } <- Om.ask
85−
86− -- "Logger component" accesses config and logs it
8753 liftEffect $ Ref.modify_ (_ <> "config-accessed-by-logger ") log
8854 let logger = { log: \msg -> Console.log $ "[" <> config.host <> "] " <> msg }
89−
90− -- "Database component" also accesses config and logs it
91− -- CRITICAL: They're both accessing the SAME context record!
9255 liftEffect $ Ref.modify_ (_ <> "config-accessed-by-database ") log
9356 let database = { query: \q -> pure [ "Result from " <> config.host ] }
94−
9557 pure { logger, database }
9658
9759 let
@@ -103,50 +65,34 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
10365 # Om.runOm ctx
10466 { exception: \_ -> pure { logger: { log: \_ -> pure unit }, database: { query: \_ -> pure [] } } }
10567
106− -- Check the access log
10768 finalLog <- liftEffect $ Ref.read accessLog
69+ finalLog `shouldEqual` "config-accessed-by-logger config-accessed-by-database "
10870
109− -- Use the resulting services to prove they work
11071 liftEffect $ result.logger.log "Testing logger from combined layer"
11172 queryResult <- liftEffect $ result.database.query "SELECT *"
112−
113− -- Both "layers" ran and accessed config via the same context
114− Console.log ""
115− Console.log "✓ RUNTIME DEDUPLICATION PROOF:"
116− Console.log $ " Access log: '" <> finalLog <> "'"
117− Console.log $ " Query result: " <> show queryResult
118− Console.log " "
119− Console.log " Both components accessed the same shared context!"
120− Console.log " The type signature OmLayer (config, accessLog) _ _ shows"
121− Console.log " that even though two components need config, there's only"
122− Console.log " ONE config in the context (not config + config)."
123− Console.log " "
124− Console.log " At runtime, both components read from the same context record,"
125− Console.log " and both wrote to the same Ref, proving deduplication works!"
126−
127− pure unit
73+ queryResult `shouldEqual` [ "Result from localhost" ]
12874
12975 describe "Scoped Layers" do
13076
13177 it "runs finalizers in reverse order on success" do
13278 log <- liftEffect $ Ref.new []
13379 let
134− layerA :: OmLayer (scope :: Scope) (a :: String) () _
80+ layerA :: OmLayer (scope :: Scope) (a :: String) ()
13581 layerA = makeScopedLayer
13682 (pure { a: "A" })
13783 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-A" ]) log)
13884
139− layerB :: OmLayer (scope :: Scope) (b :: String) () _
85+ layerB :: OmLayer (scope :: Scope) (b :: String) ()
14086 layerB = makeScopedLayer
14187 (pure { b: "B" })
14288 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-B" ]) log)
14389
144− layerC :: OmLayer (scope :: Scope) (c :: String) () _
90+ layerC :: OmLayer (scope :: Scope) (c :: String) ()
14591 layerC = makeScopedLayer
14692 (pure { c: "C" })
14793 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-C" ]) log)
14894
149− layerBC :: OmLayer (scope :: Scope) (b :: String, c :: String) () _
95+ layerBC :: OmLayer (scope :: Scope) (b :: String, c :: String) ()
15096 layerBC = combineRequirements layerB layerC
15197
15298 combined = combineRequirements layerA layerBC
@@ -158,7 +104,7 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
158104 it "runs finalizers when the callback throws" do
159105 log <- liftEffect $ Ref.new []
160106 let
161− layer :: OmLayer (scope :: Scope) (value :: String) () _
107+ layer :: OmLayer (scope :: Scope) (value :: String) ()
162108 layer = makeScopedLayer
163109 (pure { value: "acquired" })
164110 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "released" ]) log)
@@ -171,12 +117,12 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
171117 it "threads finalizers through vertical composition (provide)" do
172118 log <- liftEffect $ Ref.new []
173119 let
174− baseLayer :: OmLayer (scope :: Scope) (base :: String) () _
120+ baseLayer :: OmLayer (scope :: Scope) (base :: String) ()
175121 baseLayer = makeScopedLayer
176122 (pure { base: "base-value" })
177123 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-base" ]) log)
178124
179− upperLayer :: OmLayer (scope :: Scope, base :: String) (upper :: String) () _
125+ upperLayer :: OmLayer (scope :: Scope, base :: String) (upper :: String) ()
180126 upperLayer = makeScopedLayer
181127 ( do
182128 { base } <- Om.ask
@@ -210,12 +156,12 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
210156 it "combines scoped and non-scoped layers" do
211157 log <- liftEffect $ Ref.new []
212158 let
213− scopedLayer :: OmLayer (scope :: Scope) (scoped :: String) () _
159+ scopedLayer :: OmLayer (scope :: Scope) (scoped :: String) ()
214160 scopedLayer = makeScopedLayer
215161 (pure { scoped: "yes" })
216162 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-scoped" ]) log)
217163
218− plainLayer :: OmLayer () (plain :: String) () _
164+ plainLayer :: OmLayer () (plain :: String) ()
219165 plainLayer = makeLayer (pure { plain: "no-finalizer" })
220166
221167 combined = combineRequirements scopedLayer plainLayer
@@ -226,30 +172,29 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
226172 finalLog <- liftEffect $ Ref.read log
227173 finalLog `shouldEqual` [ "release-scoped" ]
228174
229− describe "Memoized Layers" do
175+ describe "Automatic Memoization" do
230176
231− it "builds a memoized layer only once even when used twice" do
177+ it "builds the same layer only once when used in two branches" do
232178 counter <- liftEffect $ Ref.new 0
233179 let
234− expensiveLayer :: OmLayer (scope :: Scope) (value :: String) () _
235− expensiveLayer = memoized (Proxy :: Proxy "expensive") $ makeLayer do
180+ expensiveLayer :: OmLayer (scope :: Scope) (value :: String) ()
181+ expensiveLayer = makeLayer do
236182 liftEffect $ Ref.modify_ (_ + 1) counter
237183 pure { value: "built" }
238184
239− -- Both branches independently provide expensiveLayer
240− branch1 :: OmLayer (scope :: Scope) (out1 :: String) () _
241− branch1 = consumer1 `provide` expensiveLayer
185+ branch1 :: OmLayer (scope :: Scope) (out1 :: String) ()
186+ branch1 = consumer `provide` expensiveLayer
242187 where
243− consumer1 :: OmLayer (scope :: Scope, value :: String) (out1 :: String) () _
244− consumer1 = makeLayer do
188+ consumer :: OmLayer (scope :: Scope, value :: String) (out1 :: String) ()
189+ consumer = makeLayer do
245190 { value } <- Om.ask
246191 pure { out1: value <> "-1" }
247192
248− branch2 :: OmLayer (scope :: Scope) (out2 :: String) () _
249− branch2 = consumer2 `provide` expensiveLayer
193+ branch2 :: OmLayer (scope :: Scope) (out2 :: String) ()
194+ branch2 = consumer `provide` expensiveLayer
250195 where
251− consumer2 :: OmLayer (scope :: Scope, value :: String) (out2 :: String) () _
252− consumer2 = makeLayer do
196+ consumer :: OmLayer (scope :: Scope, value :: String) (out2 :: String) ()
197+ consumer = makeLayer do
253198 { value } <- Om.ask
254199 pure { out2: value <> "-2" }
255200
@@ -264,24 +209,23 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
264209 it "memoized scoped layer runs finalizer only once" do
265210 log <- liftEffect $ Ref.new []
266211 let
267− dbLayer :: OmLayer (scope :: Scope) (db :: String) () _
268− dbLayer = memoized (Proxy :: Proxy "db") $ makeScopedLayer
212+ dbLayer :: OmLayer (scope :: Scope) (db :: String) ()
213+ dbLayer = makeScopedLayer
269214 (pure { db: "connected" })
270215 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "db-closed" ]) log)
271216
272− -- Both branches independently provide dbLayer
273− repoBranch :: OmLayer (scope :: Scope) (repo :: String) () _
217+ repoBranch :: OmLayer (scope :: Scope) (repo :: String) ()
274218 repoBranch = repo `provide` dbLayer
275219 where
276− repo :: OmLayer (scope :: Scope, db :: String) (repo :: String) () _
220+ repo :: OmLayer (scope :: Scope, db :: String) (repo :: String) ()
277221 repo = makeLayer do
278222 { db } <- Om.ask
279223 pure { repo: db <> "-repo" }
280224
281− analyticsBranch :: OmLayer (scope :: Scope) (analytics :: String) () _
225+ analyticsBranch :: OmLayer (scope :: Scope) (analytics :: String) ()
282226 analyticsBranch = analytics `provide` dbLayer
283227 where
284− analytics :: OmLayer (scope :: Scope, db :: String) (analytics :: String) () _
228+ analytics :: OmLayer (scope :: Scope, db :: String) (analytics :: String) ()
285229 analytics = makeLayer do
286230 { db } <- Om.ask
287231 pure { analytics: db <> "-analytics" }
@@ -297,8 +241,8 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
297241 it "memoized bracketLayer acquires and releases only once" do
298242 log <- liftEffect $ Ref.new []
299243 let
300− connLayer :: OmLayer (scope :: Scope) (conn :: Int) () _
301− connLayer = memoized (Proxy :: Proxy "conn") $ bracketLayer
244+ connLayer :: OmLayer (scope :: Scope) (conn :: Int) ()
245+ connLayer = bracketLayer
302246 ( do
303247 liftEffect $ Ref.modify_ (_ <> [ "acquire" ]) log
304248 pure 42
@@ -306,18 +250,18 @@ main = launchAff_ $ runSpec [ consoleReporter ] do
306250 (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release" ]) log)
307251 (\n -> pure { conn: n })
308252
309− branch1 :: OmLayer (scope :: Scope) (x :: String) () _
253+ branch1 :: OmLayer (scope :: Scope) (x :: String) ()
310254 branch1 = upper `provide` connLayer
311255 where
312− upper :: OmLayer (scope :: Scope, conn :: Int) (x :: String) () _
256+ upper :: OmLayer (scope :: Scope, conn :: Int) (x :: String) ()
313257 upper = makeLayer do
314258 { conn } <- Om.ask
315259 pure { x: show conn }
316260
317− branch2 :: OmLayer (scope :: Scope) (y :: String) () _
261+ branch2 :: OmLayer (scope :: Scope) (y :: String) ()
318262 branch2 = upper `provide` connLayer
319263 where
320− upper :: OmLayer (scope :: Scope, conn :: Int) (y :: String) () _
264+ upper :: OmLayer (scope :: Scope, conn :: Int) (y :: String) ()
321265 upper = makeLayer do
322266 { conn } <- Om.ask
323267 pure { y: show (conn + 1) }