Reputation: 12127
As I'm spending more and more time with F#, I'm trying to do things in functional ways when possible, but there is one scenario where I don't see how this could be implemented with immutable objects:
Let's imagine I have a worker that has its own states, such as the data it works on, the current status, etc. I can see that as a record and the different steps would create a new record reflecting the changes; so essentially each call to the module that manages the workers would return a new worker record.
For example:
doPhase1: worker -> worker
doPhase2: worker -> worker
But now what if these workers are managed by a 'boss' entity. And it can spawn them, remove them, but also query what they are doing:
For example:
getStatus worker -> string
At that point, it needs a handle on the worker. With a class this is very straightforward, but in a type where all the information gets replaced by a new block every time, how is that done? I can come up with a few ideas with F#, but let's say in a pure language like Haskell, how would this be handled?
If the worker is implemented like this (pseudo F#):
type Worker (status, work) =
member this.ReportStatus() = status
member this.SetThingsToGood() = Worker ("all is good!", work)
member this.SetThingsToBad() = Worker ("it's all bad", work)
member this.DoPhase1() = Worker(status, phase1Results)
member this.DoPhase2() = Worker(status, phase2Results)
While immutable, the boss entity will not be able to keep tabs on it as a new Worker is created at every change.
How are these scenarios typically handled?
A practical case here is that I have workers that run in their own threads, they react to outside events and keep their own states (they're all classes right now). And I have a general manager that can spawn them, tell them to quit or get status info on them. But I'm looking for a general answer to that problem, not how to implement specifically this.
Upvotes: 1
Views: 126
Reputation: 11142
The answer in F# is different than "in a pure language like Haskell" and this is very good. The problem with purity is that the pure functional approach kills modularity, as SICP illustrates: https://mitpress.mit.edu/sites/default/files/sicp/full-text/book/book-Z-H-24.html#%_sec_3.5.5
While using immutable types and recursive functions is idiomatic F#, F# is more pragmatic and more open to imperative code as well as to mutable variables than other functional languages. The F# core library uses mutable variables frequently. For instance:
To keep the benefits of functional immutable code, necessary mutable data structures should be kept to the minimum necessary and clearly separated or isolated. Isolated mutables are simple, their scope is just inside a function for instance. In your case you maybe want to use a mutable field inside the record to the worker inside the records. Global mutables are more subtle, they should be exposed clearly.
Also [ < ThreadStatic > ] attributed mutables are a solution to problems similar to yours or even to yours.
An example of the modularity versus functional design is the (excellent) FSCheck library that is very closely implemented after its Haskell original QuickCheck. Random generators are monadically threaded through many functions which I find painful overkill. In F# I prefer to accept the quirks of a global (thread static seeded) random generator for the benefit of much increased modularity and simpler and clearer functions in the test code.
Talking about good F# code: https://www.youtube.com/watch?v=1AZA1zoP-II
Upvotes: 2
Reputation: 27225
The general answer for how to keep state is to pass it as the argument to the recursive function.
worker :: MyState -> IO (Work) -> IO ()
worker state getWork = do
work <- getWork
let state' = process state work -- Do Something interesting
if (continue state')
than worker state' getWork
else return () -- end thread.
This pattern is in fact so common that is is wrapped up in something called the State Monad which handles all the details about passing the state around.
worker :: IO(Work) -> StateT MyState IO ()
worker getWork = do
work <- lift getWork
process work -- process :: work -> StateT MyState IO ()
whenM continue $ worker getWork -- continue :: StateT MyState IO Bool
Upvotes: 2
Reputation: 10695
If you really want workers that react to outside events then you have to use IO, whether you consider that pure or impure is another discussion. Similarly you also have to use IO to do things like forking threads. Here is some Haskell pseudocode of what that might look like:
data Worker = Worker
{ statusRef :: IORef String
, resultVar :: MVar ResultType
}
getStatus :: Worker -> IO String
getStatus w = readIORef (statusRef w)
waitForResult :: Worker -> IO ResultType
waitForResult w = takeMVar (resultVar w)
newWorker :: IO Worker
newWorker = do
statusRef <- newIORef "all is good!"
resultVar <- newEmptyMVar
forkIO $ do
... do work ...
putMVar resultVar someResult
return (Worker statusRef resultVar)
boss :: IO ResultType
boss = do
w1 <- newWorker
w2 <- newWorker
...
s1 <- getStatus w1
print s1
s2 <- getStatus w2
print s2
...
x1 <- waitForResult w1
x2 <- waitForResult w2
return (combineResults [x1, x2])
Upvotes: 0