I decided I am going to start describing Bluefin as a “capability system”. This article explains why:
GitHub - tweag/capability: Extensional capabilities and deriving combinators · GitHub ur late to the party.
The capability party is only just getting started!
If I ever write an effect library it will be called knobs. [“handle” ~~> knob]
I like this change. It clarifies how to think about Bluefin to me. I’m not exactly sure why. Names can be powerful things.
Nice, thanks for the feedback and I’m glad you like the change. I agree that names can be powerful things. Like it or not, people often lean on them for intuition, sometimes unconsciously. Choosing good names can help a lot.
I remember I was a bit confused by Tom’s “A History of Effect Systems” talk w/r/t ReaderT IO.
@tomjaguarpaw you mentioned for the ReaderT pattern that “everything is now in IO” / “no encapsulation”. But isn’t the ReaderT-IO-pattern used together with “capability classes” or “tagless final” or whatever people call it, so you only have the effects that are in your constraints?
RIO suggests “Has classes”, which I guess is what you mean by “capability classes”:
class HasConfig env where
configL :: Lens' env Config -- more on this in a moment
myFunction :: HasConfig env => RIO env Foo
Here’s an example from the Stack source. You can use withEnvConfig to run a RIO EnvConfig blog inside a RIO Config, by providing some extra info that it needs, specifically the NeedTargets and the BuildOptsCLI.
But there’s nothing to prevent the EnvConfig from leaking:
dodgy :: NeedTargets -> BuildOptsCLI -> RIO Config ()
dodgy nt opts = do
envConfig <- withEnvConfig nt opts ask
let _ = envConfig :: EnvConfig
...
Maybe not such a big deal when it’s a compiler configuration, but if it was a block that’s supposed to locally escalate privileges then that could be a big deal.
Thank you for the example, I see that this could happen. I haven’t used RIO specifically, what I was imagining was rather having functions like
f :: (MonadRandom m) => m Int
It doesn’t really matter if this would be implemented with a ReaderT IO stack in the end, I am still constrained to only use MonadRandom functions in f right? I thought this is what people did with the ReaderT pattern, maybe with domain-specific classes instead of MonadState / MonadRandom etc. (but I have no idea whether that is true
).
Yes, there’s no problem with what you are constrained to do in an effectful operation. The problem is whether effectful operations can leak out of the scope of the handler that introduces the effect. withEnvConfig is an example of a handler and my example above shows that the operations it introduces can leak out (by extracting an EnvConfig).
EDIT: I realised I’ve written an article on exactly this topic: Bluefin prevents handles leaking
Capabilities also combine designation and authority and ideally support attenuation. The Bluefin value for a state effect is a capability to one specific mutable state. Many other effects add some ambient authority.
If I have a KeyValueStore effect, it would be nice to turn it into a fine-grained capability that I can attenuate so that I can grant the authority to only read a specific key, or write a specific key, or create a new key.
There is an interesting comment on Lobste.rs, where I think the point is summarized in this sentence:
The Pony StartProcessAuth is an authority not a capability, because it doesn’t designate the program to be started.
I didn’t know about this authority/capability terminology.
Nor me, thanks for sharing! I don’t see why that distinction is useful but perhaps someone will be able to explain it to me.
Sure, Bluefin can do that:
-- ghci> example
-- fromList [("good bye","C++"),("hello","world")]
example :: Map String String
example = runPureEff $ do
let s =
Map.fromList
[ ("good bye", "C++"),
("hello", "kephas")
]
Static.evalModify s $ \map -> do
let _ = map :: Static.Modify (Map String String) _
toDynamic map $ \dynamicMap -> do
let _ = map :: Static.Modify (Map String String) _
attenuateAtKey "hello" dynamicMap $ \helloValue -> do
-- helloValue only allows us to read and write the value at
-- key "hello"
let _ = helloValue :: DynamicModify (Maybe String) _
attenuateModifyToWrite helloValue $ \helloValueWrite -> do
-- helloValueWrite only allows us to write the value at key
-- "hello"
let _ = helloValueWrite :: WriteOnly (Maybe String) _
write helloValueWrite (Just "world")
Static.get map
Full code
{-# LANGUAGE GHC2021 #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# OPTIONS_GHC -Wno-partial-type-signatures #-}
import Bluefin.Capability.Modify qualified as Static
import Bluefin.Compound
( Handle,
OneWayCoercible,
OneWayCoercibleHandle (MkOneWayCoercibleHandle),
makeOp,
mapHandle,
oneWayCoercibleImpl,
oneWayCoercibleTrustMe,
useImplIn,
useImplUnder,
)
import Bluefin.Eff (Eff, runPureEff, (:&), (:>))
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
-- ghci> example
-- fromList [("good bye","C++"),("hello","world")]
example :: Map String String
example = runPureEff $ do
let s =
Map.fromList
[ ("hello", "kephas"),
("good bye", "C++")
]
Static.evalModify s $ \map -> do
let _ = map :: Static.Modify (Map String String) _
toDynamic map $ \dynamicMap -> do
let _ = map :: Static.Modify (Map String String) _
attenuateAtKey "hello" dynamicMap $ \helloValue -> do
-- helloValue only allows us to read and write the value at
-- key "hello"
let _ = helloValue :: DynamicModify (Maybe String) _
attenuateModifyToWrite helloValue $ \helloValueWrite -> do
-- helloValueWrite only allows us to write the value at key
-- "hello"
let _ = helloValueWrite :: WriteOnly (Maybe String) _
write helloValueWrite (Just "world")
Static.get map
toDynamic ::
(e1 :> es) =>
Static.Modify s e1 ->
(forall e. DynamicModify s e -> Eff (e :& es) r) ->
Eff es r
toDynamic m k =
useImplIn k $
DynamicModify
{ getImpl = Static.get m,
putImpl = Static.put m
}
get :: (e :> es) => DynamicModify s e -> Eff es s
get h = makeOp (getImpl (mapHandle h))
put :: (e :> es) => DynamicModify s e -> s -> Eff es ()
put h s = makeOp (putImpl (mapHandle h) s)
modify :: (e :> es) => DynamicModify s e -> (s -> s) -> Eff es ()
modify h f = get h >>= put h . f
data DynamicModify s e = DynamicModify
{ getImpl :: forall e'. Eff (e' :& e) s,
putImpl :: forall e'. s -> Eff (e' :& e) ()
}
deriving (Handle) via OneWayCoercibleHandle (DynamicModify s)
instance (e :> es) => OneWayCoercible (DynamicModify s e) (DynamicModify s es) where
oneWayCoercibleImpl = oneWayCoercibleTrustMe $ \h ->
DynamicModify
{ getImpl = useImplUnder (getImpl h),
putImpl = useImplUnder . putImpl h
}
data ReadOnly s e = ReadOnly
{ readImpl :: forall e'. Eff (e' :& e) s
}
deriving (Handle) via OneWayCoercibleHandle (ReadOnly s)
instance (e :> es) => OneWayCoercible (ReadOnly s e) (ReadOnly s es) where
oneWayCoercibleImpl = oneWayCoercibleTrustMe $ \h ->
ReadOnly {readImpl = useImplUnder (readImpl h)}
data WriteOnly s e = WriteOnly
{ writeImpl :: forall e'. s -> Eff (e' :& e) ()
}
deriving (Handle) via OneWayCoercibleHandle (WriteOnly s)
instance (e :> es) => OneWayCoercible (WriteOnly s e) (WriteOnly s es) where
oneWayCoercibleImpl = oneWayCoercibleTrustMe $ \h ->
WriteOnly {writeImpl = useImplUnder . writeImpl h}
attenuateModifyToRead ::
(e1 :> es) =>
DynamicModify s e1 ->
(forall e. ReadOnly s e -> Eff (e :& es) r) ->
Eff es r
attenuateModifyToRead m k =
useImplIn k $
ReadOnly
{ readImpl = get m
}
attenuateModifyToWrite ::
(e1 :> es) =>
DynamicModify s e1 ->
(forall e. WriteOnly s e -> Eff (e :& es) r) ->
Eff es r
attenuateModifyToWrite m k =
useImplIn k $
WriteOnly
{ writeImpl = put m
}
attenuateAtKey ::
(Ord k, e1 :> es) =>
k ->
DynamicModify (Map k v) e1 ->
(forall e. DynamicModify (Maybe v) e -> Eff (e :& es) r) ->
Eff es r
attenuateAtKey key m body =
useImplIn body $
DynamicModify
{ getImpl = Map.lookup key <$> get m,
putImpl = \mv ->
modify m (Map.alter (const mv) key)
}
write :: (e :> es) => WriteOnly s e -> s -> Eff es ()
write h s = makeOp (writeImpl (mapHandle h) s)
read :: (e :> es) => ReadOnly s e -> Eff es s
read h = makeOp (readImpl (mapHandle h))
(attenuateAtKey could be generalized to attenuateByLens)