Question

In the OO world, I have a class (let's call it "Suggestor") that implement something approaching a "Strategy Pattern" to provide differing implementations of an algorithm at runtime. As an exercise in learning Haskell, I want to rewrite this.

The actual use-case is quite complex, so I'll boil down a simpler example.

Let's say I have a class Suggester that's takes a list of rules, and applies each rule as a filter to a list of database results.

Each rule has three phases "Build Query", "Post Query Filter", and "Scorer". We essentially end up with an interface meeting the following

buildQuery :: Query -> Query
postQueryFilter :: [Record] -> [Record]
scorer :: [Record] -> [(Record, Int)]

Suggestor needs to take a list of rules that match this interface - dynamically at run time - and then execute them in sequence. buildQuery() must be run across all rules first, followed by postQueryFilter, then scorer. (i.e. I can't just compose the functions for one rule into a single function).

in the scala I simply do

// No state, so a singleton `object` instead of a class is ok
object Rule1 extends Rule {
  def buildQuery ...
  def postQueryFilter ...
  def scorer ...
}

object Rule2 extends Rule { .... }

And can then initialise the service by passing the relevant rules through (Defined at runtime based on user input).

val suggester = new Suggester( List(Rule1, Rule2, Rule3) );

If the rules were a single function, this would be simple - just pass a list of functions. However since each rule is actually three functions, I need to group them together somehow, so I have multiple implementations meeting an interface.

My first thought was type classes, however these don't quite seem to meet my needs - they expect a type variable, and enforce that each of my methods must use it - which they don't.

No parameters for class `Rule`

My second thought was just to place each one in a haskell module, but as modules aren't "First Class" I can't pass them around directly (And they of course don't enforce an interface).

Thirdly I tried creating a record type to encapsulate the functions

data Rule = Rule { buildQuery :: Query -> Query, .... etc }

And then defined an instance of "Rule" for each. When this is done in each module it encapsulates nicely and works fine, but felt like a hack and I'm not sure if this is an appropriate use of records in haskell?

tl;dr - How do I encapsulate a group of functions together such that I can pass them around as an instance of something matching an interface, but don't actually use a type variable.

Or am I completely coming at this from the wrong mindset?

Was it helpful?

Solution 2

Just generate a single Rule type as you did

data Rule = Rule
  { buildQuery :: Query -> Query
  , postQueryFilter :: [Record] -> [Record]
  , scorer :: [Record] -> [(Record, Int)]
  }

And build a general application method—I'm assuming such a generic thing exists given that these Rules are designed to operate independently over SQL results

applyRule :: Rule -> Results -> Results

Finally, you can implement as many rules as you like wherever you want: just import the Rule type and create an appropriate value. There's no a priori reason to give each different rule its own type as you might in an OO setting.

easyRule :: Rule
easyRule = Rule id id (\recs -> zip recs [1..])

upsideDownRule :: Rule
upsideDownRule = Rule reverse reverse (\recs -> zip recs [-1, -2..])

Then if you have a list of Rules you can apply them all in order

applyRules :: [Rule] -> Results -> Results
applyRules []     res = res
applyRules (r:rs) res = applyRules rs (applyRule r res)

which is actually just a foldr in disguise

applyRules rs res = foldr applyRule res rs

foo :: Results -> Results
foo = applyRules [Some.Module.easyRule, Some.Other.Module.upsideDownRule]

OTHER TIPS

In my opinion your solution isn't the "hack", but the "strategy pattern" in OO languages: It is only needed to work around the limitations of a language, especially in case of missing, unsafe or inconvenient Lambdas/Closures/Function Pointers etc, so you need a kind of "wrapper" for it to make it "digestible" for that language.

A "strategy" is basically a function (may be with some additional data attached). But if a function is truly a first class member of the language - as in Haskell, there is no need to hide it in the object closet.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top