Causality
Causality

Reputation: 1123

Haskell design best practice for multiple modules assocated with similar set of functions

Say the functions foo, bar noo are the basics in my program. Furthermore, these functions can be implemented in different way in different scenarios (foo1, bar1, foo2, bar2, etc), though foo1 and foo2 still have the same input and output types. According to some input or configuration, the program use foo1, bar1 in some scenario, while in another scenario, foo2, bar2.

I could have just defined them as described above, appending suffix (1,2,3..) to foo, bar, noo. However, this is not pretty as the suffix could be long; neither does it allow special binding of foo1 with bar1 (vs. bar2).

An alternative would be treating each scenario as a separate Module. Now foo, bar, noo for each case are nicely hold together, and the ugly suffix is avoided. However, this introduces many files when having one file per Module. Another downside of this approach is that these Modules are completed separated even though they do share some similarity (e.g. three functions).

A typeclass solution would be appreciated but not came across my mind, as the different foos of different scenarios have the same input and output.

I am wondering is there any Haskell best practice for the problem to avoid the aforementioned shortcomings of these approaches.

foo1 :: Double -> Double
bar1 :: Int -> Int
noo1 :: [Int] -> [Int]

foo2 :: Double -> Double
bar2 :: Int -> Int
noo2 :: [Int] -> [Int]

...

foo9 :: Double -> Double
bar9 :: Int -> Int
noo9 :: [Int] -> [Int]

EDIT: I guess it is relevant for the discussion to explain how I would approach it through Java Interface (A few nice, but conceptual-level, discussion of Java interface and Haskell typeclass can be found at this post and here.) Java interface and class can be complicated for many cases, but here the overloading is actually concise.

interface Scenario {
  double     foo(double d);
  int        bar(int i);
  Array<int> noo(Array<int> a);
}

class UseScenario {
  void use(Scenario ss) {
    ss.foo(...);
    ss.bar(...);
    ss.noo(...);
  }
}

class S1 implements Scenario {
  double     foo(double d) {...};
  int        bar(int i) {...};
  Array<int> noo(Array<int> a) {...};
}

class S2 implements Scenario {
  double     foo(double d) {...};
  int        bar(int i) {...};
  Array<int> noo(Array<int> a) {...};
}

Upvotes: 4

Views: 466

Answers (1)

David Miani
David Miani

Reputation: 14678

One good way would be to put all the functions into a single data type. Then have different values of that type for each different strategy. Finally, choose a default strategy, and link the actual functions tho the default strategy (for ease of use). Eg:

module MyModule where


data Strategy  = Strategy {
    fooWithStrategy :: Double -> Double
  , barWithStrategy :: Int -> Int
  , nooWithStrategy :: [Int] -> [Int]
  }

defaultStrategy :: Strategy
defaultStrategy = Strategy { 
    fooWithStrategy = (*2)
  , barWithStrategy = (+2)
  , nooWithStrategy = id
  }

foo :: Double -> Double
foo = fooWithStrategy defaultStrategy

bar :: Int -> Int
bar = barWithStrategy defaultStrategy

noo :: [Int] -> [Int]
noo = nooWithStrategy defaultStrategy

tripleStrategy :: Strategy
tripleStrategy = Strategy {
    fooWithStrategy = (*3)
  , barWithStrategy = (*3)
  , nooWithStrategy = \x -> x ++ x ++ x
  }

customAddStrategy :: Int -> Strategy
customAddStrategy n = Strategy {
    fooWithStrategy = (+ (fromIntegral n))
  , barWithStrategy = (+ n)
  , nooWithStrategy = (n :)
  }

This allows a number of useful features:

  1. Customizable strategies (eg the customAddStrategy). You could also mix and match strategies, eg newStrat = defaultStrategy { nooWithStrategy = nooWithStrategy tripleStrategy, fooWithStrategy = (*4) }
  2. Users can switch strategies at run time
  3. Defaults (ie foo, bar and noo) are available for users new to the library
  4. Easily extended with more strategies by you or other users.

Upvotes: 6

Related Questions