5

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?

James Davies
  • 9,602
  • 5
  • 38
  • 42
  • 8
    In what way does this feel like a hack? I think this is a perfectly acceptable way of using a record. – YellPika Feb 10 '14 at 12:37
  • Haskell records seem like they are designed to hold data, not functions. Is it acceptable idiomatic Haskell to pass around functions as part of a record? I'm possibly blinded by the similarity between records and case classes, and putting too much weight behind the OO idea that there's a specific construct to do this. – James Davies Feb 10 '14 at 12:54
  • 7
    Functions are data. I.e., they are first-class data. They can be passed, returned, and stored. When this no longer seems odd, you will be well on the way to OO enlightenment. In OO functions are usually not first-class data and so we wrap functions in objects as in the Command and Strategy patterns. – Theodore Norvell Feb 10 '14 at 13:16
  • Makes complete sense, thanks. I'm used to first class functions in Scala, but its OO lineage still syntactically create a differentiation between data and functions as different types of language structures. Since Haskell has type-classes, which are *almost* what I wanted, I assumed there was a specific Haskell syntax to do this above and beyond records. I presume then if I want to include state as well, I'd move to type-classes instead of throwing both data and functions together? – James Davies Feb 10 '14 at 13:23
  • 6
    Again, **functions _are_ data**. There's no throwing them together, because they're already the same stuff anyway. If you want to include state as well you might want to use something like `lenses`, but that has little to do with type classes. – leftaroundabout Feb 10 '14 at 13:30
  • 1
    @JamesDavies If you wanted to include state, you should just wrap your computations in the `State` monad, or if needed the `StateT` monad transformer. I think you might be a bit confused about the purpose of typeclasses. They're more akin to OOP interfaces than anything else, but they give you more power and flexibility than in most OO languages. If a type is a member of a typeclass, it guarantees that you can perform certain operations on that type. They inherently have nothing to do with state. – bheklilr Feb 10 '14 at 13:33
  • 2
    @JamesDavies No, you carry on throwing them in a record - stateful calculations are monadic data, but data nevertheless. There's no special syntax for this, because it's so easy in Haskell. You don't need a typeclass, it's not a hack, it's just a record with some functions etc in. – not my job Feb 10 '14 at 14:34
  • 2
    You don't need a strategy pattern in Haskell, because functions, even stateful ones are first class values, and you can pass them around in a record/ADT/as values/as parameters or arguments. – not my job Feb 10 '14 at 14:38

2 Answers2

9

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.

Landei
  • 54,104
  • 13
  • 100
  • 195
  • 4
    Exactly. The following translations may be helpful when learning FP by un-learning OO: Composite pattern = algebraic data type; Visitor pattern = fold(r), Iterator pattern = laziness. – d8d0d65b3f7cf42 Feb 10 '14 at 19:18
  • Thanks. The end result is surprisingly still pretty much identical to a "strategy" in OO world. A bunch of functions matching an interface contained in a data structure. Whether it's a record, a struct, or a class, the design ends up quite similar (So long as it's stateless and pure in the first place). A rose by any other name.... Of course if it was wrapping a single function then the struct would not be needed as it could be passed directly, but this is the same in many OO languages as well. I'm having to unlearn a lot less than I thought (so far). – James Davies Feb 11 '14 at 06:58
  • That's true only for single-method strategies. – leventov Feb 19 '15 at 23:23
7

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]
J. Abrahamson
  • 72,246
  • 9
  • 135
  • 180
  • 1
    Also, whenever thinking about OO design patterns in Haskell, please take a look [at this post by E.Z. Yang](http://blog.ezyang.com/2010/05/design-patterns-in-haskel/). – J. Abrahamson Feb 10 '14 at 21:49