blockfrost / blockfrost-haskell

Haskell SDK for Blockfrost.io
Apache License 2.0
28 stars 21 forks source link

Having a Tagless Final version of the API #10

Closed nhenin closed 2 years ago

nhenin commented 2 years ago

Hi, It's quite difficult to compose logic with a rigid datatype like :

type ClientConfig = (ClientEnv, Project)
type BlockfrostClient =
  ExceptT BlockfrostError
    (ReaderT ClientConfig IO)

I would have expected using the API that way :

getTx :: BlockfrostClient m => TxHash -> m Transaction
-- vs
getTx :: TxHash -> BlockfrostClient Transaction

And be able to compose my logic that way :

myOwnLogic ::  (BlockfrostClient m , OtherClient m ) => Hash -> m (Tx,Tx')
myOwnLogic  hash = do 
  tx <- getTx hash
  tx' <- getTxFromOtherClient hash
  return (tx,tx')

what do you think ?

sorki commented 2 years ago

Hi, sounds good to me but to be honest, I'm not sure how to convert it to tagless final right away. If you have good example I can take a look at or draft a solution here it would help a lot.

Regarding breaking the API - I don't think there are that many users of this library so it should be fine.

sorki commented 2 years ago

I've remembered about https://jproyo.github.io/posts/2019-03-17-tagless-final-haskell.html and it makes sense, having a typeclass BlockfrostClient (or maybe just Blockfrost to avoid name clashes) with instance that can possibly call the fixed mtl client functions. What I'm not sure about is ReaderT containing Project, maybe we can just wrap our old BlockfrostClient type though. I'll play with it a bit!

sorki commented 2 years ago

Hi, I've opened #11 which adds MonadBlockfrost class allowing you to use a custom monad instead of the default BlockfrostClient.

Using this I was able to compose it with example TestapiM like this:

class Monad m => TestapiM m where
  mGetTest :: m Bool

sample :: (MonadBlockfrost m, TestapiM m) => m ()
sample = pure ()

type GlobalConfig = (Bool, ClientConfig)
type App = ReaderT GlobalConfig (ExceptT ClientError IO)

runApp :: ClientConfig -> App a -> IO (Either ClientError a)
runApp globalC = runExceptT . flip runReaderT (True, globalC)

instance TestapiM App where
  mGetTest = fst <$> ask

instance MonadBlockfrost App where
  liftBlockfrostClient act = getConf >>= \(env, _prj) -> 
    (liftIO $ runClientM act env)
  getConf = snd <$> ask

Would this work for you?