Bluefin versus OOP

OK, if you like working that way then more power to you! I’ll stick with libraries that are open to extension.

7 Likes

osa1:

OOP is not that bad, actually

The Expression Problem (1998):

  • favouring (functional) code over data (constructors) makes it more difficult to add extra constructors later to existing functions;

  • favouring data (objects) over (method) code makes it more difficult to add extra methods later to existing objects.

It’s something to remember when reading articles like OOP is not that bad, actually

1 Like

Is this the expression problem?

Perhaps it’s somewhat related. According to Wikipedia, in the expression

The goal is to define a data abstraction that is extensible both in its representations and its behaviors

I’m talking about the need to be extensible in behaviours. When @vshabanov says that you can define data Logger = Stdout | File Handle and then add further constructors if you like, that is about being extensible in representation.

For me the former is satisfactory and the latter unsatisfactory. I’m not really trying to obtain both at once.

In effects systems the representations are the effects and their operations and the behaviors are the handlers. This is more obvious if you implement them using the Free monad data type, for example the paper Data Types à la Carte essentially presents a solution to the expression problem and it ends with a Free monad based implementation of algebraic effects.

But I think even Bluefin can be understood to solve the expression problem in this way. Combining multiple effects into one Eff data type is representation extensibility and handling effects in multiple different ways is behavior extensibility.

1 Like

I’d reiterate that the premise of working “without being in control of the interface definition” is wrong and leads to code rot.

We shouldn’t base our designs on the approach that leads to code rot.

Write something basic first and give it a go. YAGNI. Once you have several real common use cases, you can generalise. And there are many ways to do that, including changing the interface.

It’s convenient to stick with libraries that are open to extension, but how do you build them? With iterations, which may involve forks and PRs.

If adding a constructor surprises you, take a look at fast-logger. It has a generic LogCallback constructor that wasn’t there from the beginning. Someone made a PR.

I would like to offer another alternative, which consists in using a separate data structure to do all the wiring.

The components, are defined as record of functions:

data Logger = Logger
  { log :: Text -> Severity -> IO ()
  }
 
data Database = Database
  { executeQuery :: Text -> IO ()
  }

There are constructor functions to build them (those functions can be effectful if needed):

-- | A production database using a Logger
newDatabase :: Logger -> Database
newDatabase logger = Database {..} where
  executeQuery :: Text -> IO ()
  executeQuery q = do
    log logger ("executing query " <> q) Info
    print "do it

And a registry to wire them in different scenarios, in an incremental way (no need to change existing configurations):

prodRegistry =
     fun newDatabase
  <: fun newLogger

prodDatabase = make @Database prodRegistry

-- Don't log anything for those tests
testRegistry1 =
     fun noLogger
  <: prodRegistry

testDatabase1 = make @Database testRegistry1

-- Only log some messages for those tests
testRegistry2 =
  tweak @Logger (limitSeverity Info) prodRegistry

testDatabase2 = make @Database testRegistry2

-- Only log some messages for those tests
-- Specify the severity as a separate value
testRegistry3 =
     fun newLimitedLogger
  <: val Info
  <: prodRegistry

testDatabase3 = make @Database testRegistry3

You can even declare that you want to limit the logging severity only to some components of your application graph and not others.

You can find a running example here if you want to play with it:

In this approach we start with interfaces, there’s no “concrete class” as @osa1 would say, but I don’t see how this is a problem. Eventually we get to same goal: clients like Database don’t need to change their code if there’s a new implementation.

I think that the very important point is that the _wiring_code can be modified incrementally. In @osa1’s example MyApp.testingSetup() redefines the whole wiring that MyApp() does.

There is a similar issue with using effect libraries to describe application components. At some stage you end up with a stack of:

   runEffect1
   runEffect2
   runEffect3
   runEffect4
   runEffect5

and if you just want to modify the interpreter for effect3 you have to rewrite the whole stack. And if you want to modify how that effect is interpreted, just when it is used in the context of effect4 but not in the context of effect5 (say logging differently for the database or HTTP calls), then you can’t do it.

BTW I’m not saying that registry is a perfect solution, I’m just trying to highlight some good (IMO) aspects of it!

7 Likes

registry is great. I’ve seen it mentioned before and liked the idea.

It’s cool that it’s completely orthogonal:

-- one can write
a :: IO A
b :: IO B
c :: A -> B -> C
-- and use them directly
main = c <$> a <*> b
-- or put them in the registry

I’m not sure what to do with a typical use case where functions have both static “component” and dynamic parameters and a dynamic call graph:

foo :: A -> C -> String -> IO ()
foo a c s = ... x <- bar a; baz c (if x then 1 else qux a s)
bar :: A -> IO Bool
baz :: C -> Int -> String
qux :: A -> String -> Int

But if the system can be decomposed into a static component graph, then registry can be nice.

I was too surprised to see MyApp.testingSetup() followed by “MyApp methods didn’t have to change at all.”

1 Like

Hi Vladimir,

I would do the following:

  1. Request-scoped dependency

A dependency might only be used depending on requests. In that case I add it and use it only when necessary:

data TransactionEngine = TransactionEngine {
  processTransaction :: Transaction -> IO Report
}

newTransactionEngine :: Logger -> Database -> Emailer -> TransactionEngine
newTransactionEngine logger database emailer = TransactionEngine {..} where 
   processTransaction t = do
     -- process the transaction
     report <- doSomething
     when sendReport transaction do 
       sendEmail emailer report
     saveReport database report
  1. Configuration-based dependency

Sometimes the exact set of components to use are known at runtime, based on configuration parameters. In that case I create 2 registries:

appRegistry = if isSandbox config then sandboxRegistry else prodRegistry
application = make @Application appRegistry
1 Like