tomjaguarpaw / bluefin

MIT License
46 stars 6 forks source link

Can a complete example akin to a "Getting started" be added? #18

Closed cideM closed 2 weeks ago

cideM commented 2 weeks ago

Maybe I just didn't know where to look but there seems to be no complete, self contained example, such as a Main.hs file showing necessary LANGUAGE pragmas and where to get typical imports from. It's great that the documentation has examples, and I think this project would be even more approachable if it had one blob of code you can just copy into your editor and modify it.

tomjaguarpaw commented 2 weeks ago

How about Internal/Examples.hs? This is the file from which the Haddock examples are sourced. Does this file itself satisfy your requirements? If so I can link it from the README (or elsewhere if you have a suggestion). If not then what would you suggest changing to satisfy your requirements?

cideM commented 2 weeks ago

I think that this file is too big. Additionally, depending on Bluefin.Internal seems like something people would want to avoid. Since it's hard to know what an average use case for this library is, I'd suggest to take the first example from the introduction and turn it into a mostly self contained example that includes a main function (and whatever language extensions are required). It's a pretty common thing in Go packages and makes it trivial to get started.

Alternatively, using the smallest possible example that uses IO might be closer to real world usage.

tomjaguarpaw commented 2 weeks ago

Ah yes, I see. You want some sort of getting started template?

cideM commented 2 weeks ago

In retrospect that's a much better title for this issue and yes that's what I had in mind! The documentation is already very good, especially for a Haskell project. I'm currently going through that getting started phase and while I can piece things together by looking at the various modules and figuring out where I can get each import from, following compiler errors for missing language extensions, and so on, but having all that wrapped up in one convenient example would mean the time it takes to go from "what's bluefin?" to having a runnable, editable example is just a few seconds.

tomjaguarpaw commented 2 weeks ago

OK great. Since you're already piecing something together, do you have something you could paste here (whether it works or not?) and we can collaboratively edit it into the form you'd like?

cideM commented 2 weeks ago

Here's an example showing what a database interaction could look like. I admit that this is biased towards web development and that I went against what I suggested above (using the counter example). Using a custom effect seems like something almost every user of the library would want. At least it should probably include some IO. Do you think it's too involved?

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeOperators #-}

module Main where

import Bluefin.Compound
import Bluefin.Eff (Eff, (:&), (:>))
import qualified Bluefin.Eff as BF
import Bluefin.Exception (Exception)
import qualified Bluefin.Exception as BF
import Bluefin.IO (IOE)
import qualified Bluefin.IO as BF

newtype DbHandle = DbHandle String deriving (Show)

newtype UserId = UserId String deriving (Show, Eq)

newtype User = User String deriving (Show)

data DbEff es = MkDbEff
  { queryImpl :: DbHandle -> UserId -> Eff es User
  }

query :: (e :> es) => DbEff e -> DbHandle -> UserId -> Eff es User
query db dbHandle userId = useImpl $ queryImpl db dbHandle userId

runDbEffIo ::
  forall exEff dbEff es r.
  (exEff :> es, dbEff :> es) =>
  Exception String exEff ->
  IOE dbEff ->
  (forall e. DbEff e -> Eff (e :& es) r) ->
  Eff es r
runDbEffIo ex _ fn =
  useImplIn
    fn
    ( MkDbEff
        { queryImpl = \_ userId -> do
            if userId == UserId "1"
              then pure $ User "Alice"
              else BF.throw ex "not found"
        }
    )

main :: IO ()
main = do
  let dbHandle = DbHandle "db"

  result <- BF.runEff $ \io -> BF.try $ \ex ->
    runDbEffIo ex io $ \db -> do
      u1 <- query db dbHandle (UserId "1")
      BF.effIO io $ print u1
      u2 <- query db dbHandle (UserId "2")
      BF.effIO io $ print u2

  case result of
    Left err -> print err
    Right _ -> print "success"

Turning the counter example into a runnable file adds very little

module Main where

import qualified Bluefin.Eff as BF
import qualified Bluefin.State as BF
import Control.Monad (when)

main :: IO ()
main = print $
  BF.runPureEff $
    BF.evalState (5 :: Integer) $ \state -> do
      counter <- BF.get state
      when (counter < 10) $
        BF.modify state (+ 10)
      BF.get state
tomjaguarpaw commented 2 weeks ago

Here's an example showing what a database interaction could look like

Ah great, I think that looks nice. Should I incorporate that straight into an template package/module in the repository?

Turning the counter example into a runnable file adds very little

Do you mean you don't think it's worth adding that example?

cideM commented 2 weeks ago

Ah great, I think that looks nice. Should I incorporate that straight into an template package/module in the repository?

That sounds good 🙏🏻

Do you mean you don't think it's worth adding that example?

Yes, I think having one runnable example should be enough, since especially the small snippets need very little to no additional code.

By the way, effect systems were always pretty intimidating because of all the type magic going on. Bluefin is comparatively simple and the examples and documentation really help. Great work ⭐

tomjaguarpaw commented 2 weeks ago

@cideM could you have a look at this? https://github.com/tomjaguarpaw/bluefin/pull/19

By the way, effect systems were always pretty intimidating because of all the type magic going on. Bluefin is comparatively simple and the examples and documentation really help. Great work ⭐

That's really great to hear! That was my hope :) Please do continue to give feedback about how I can make it even simpler or to help people get started.

cideM commented 2 weeks ago

Closed in #19 3001d7ca840d552f3ca6470f32d0103e1b3e6810