@@ -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 >->