Reputation: 7183
I have a program that's almost pure mathematical computation. The problem is that some of those computations operate on monte carlo generated values.
It seems like I have two design options:
Either all my computation functions take additional parameter which contains a pre-generated monte carlo chain. This lets me keep pure functions everywhere, but since there's functions that call other functions this adds a lot of line noise to the code base.
The other option is to make all the computation functions monadic. This seems unfortunate since some of the functions aren't even using those random values they're just calling a function which calls a function which needs the random values.
Is there any guidance regarding the preferred design here? Specifically, the separation of monadic / non-monadic functions in the code where monte carlo values are concerned?
Upvotes: 3
Views: 103
Reputation: 30227
The other option is to make all the computation functions monadic. This seems unfortunate since some of the functions aren't even using those random values they're just calling a function which calls a function which needs the random values.
I would suggest following this approach, and I disagree with your assessment that it's "unfortunate." What monads are good at precisely is separating your pure code from your side effecting code. Your pure functions can just have pure types, and the Functor
/Applicative
/Monad
methods serve to "hook them up" with the random generation parts. Meditate on the signatures of the standard operations (here specialized to some idealized Random
monad type):
-- Apply a pure function to a randomly selected value.
fmap :: (a -> b) -> Random a -> Random b
-- Apply a randomly selected function to a randomly selected argument.
-- The two random choices are independent.
(<*>) :: Random (a -> b) -> Random a -> Random b
-- Apply a two-argument function to a randomly selected arguments.
-- The two random choices are independent.
liftA2 :: (a -> b -> c) -> Random a -> Random b -> Random c
-- Make a `Random b` choice whose distribution depends on the value
-- sampled from the `Random a`.
(>>=) :: Random a -> (a -> Random b) -> Random b
So the reformulated version of your approach is:
Functor
/Applicative
/Monad
class operations.Random
type superfluously, figure out how to factor the Random
part out using those classes' operations (or the copious utility functions that exist for them).This is not specific to random number generation, by the way, but applies to any monad.
You might enjoy reading this article, and might want to check out the author's random generation monad library:
I doubt you need to follow the article's approach of using free monads for modeling, but the conceptual bits about probability distribution monads will likely be of some help.
Upvotes: 2
Reputation: 3567
tl;dr: Consider to abstract the random function generator and pass it as an argument. Haskells type classes should help you to hide that abstraction as much as possible.
Unfortunately, there is no silver bullet here. Since you are using side effects, your "functions" simply aren't functions in the proper sense. Haskell does not allow you to hide that fact (which makes up the largest part of its safety guarantees). So in some way you will need to express this fact. You also seem to confuse the difference between monadic operations and (plain) functions: A function that (indirectly) uses random values is implicitly monadic. A non-monadic function can always be used inside a monadic operation. So you should probably implement all truly non-monadic functions as such and see how far that carries.
As a completely unrelated side-note: If lazyness is not a requirement and Haskells strong safety is too much a burden for you, but you still want to write (mostly) functional code, you could give OCaml a try (or any other ML dialect for that matter).
Upvotes: 0