First of all; thank you for taking the time to read my question. If there is any more information you need or would like me to change something please let me know.
When I pass in an array handler function the type inference does not work, but when I add the function to the module instead of injecting it then it does work.
Tried adding type annotation but that's just ignored and F# warns about code being less generic when calling it the first time and then errors out with wrong type the second time.
But if I change:
let handleAction
//following does not work, comment out next line
(mapItems : 'a -> 'b [] -> int -> ('b -> 'a -> 'b) -> 'b [])
to
let handleAction
//following does not work, comment out next line
(notPassed : 'a -> 'b [] -> int -> ('b -> 'a -> 'b) -> 'b [])
Then it works just fine. Trying to remove upwards dependencies but can't get F# to understand the type.
let mapItems
action
state
index
handlerfn =
state
|> Array.indexed
|> Array.map (
fun (i, item) ->
if index < 0 then
handlerfn item action
else if i = index then
handlerfn item action
else
item)
//Mediator calling the handler for the action
let handleAction
//following does not work, comment out next line
(mapItems : 'a -> 'b [] -> int -> ('b -> 'a -> 'b) -> 'b [])
//notPassedIn //uncomment this and it works
//even though mapItems here and mapItems
//passed in are the exact same code
state
action =
match action with
|Pausable action -> //actions specific to pausable stopwatch
let handler =
mapItems
action //warning: less generic
state
action.index
match action.``type`` with
//... pausable actions (added to support pause/continue)
| StopWatch action -> //actions from stop watch
let handler =
mapItems
action//error: wrong type
state
action.index
match action.``type`` with
//...handling stopwatch actions
Full code is here: https://github.com/amsterdamharu/programmingbook/tree/example8
(*
stopwatch module
*)
//types
type SWActionType =
| Start of int
type StopWatchAction = {
``type``:SWActionType
//there may be more than one stopwatch in the application
index:int
}
type StartDate =
| NoStartDate
| Date of int
type SingleStopWatchState = {
status:string
}
type StopWatchState = SingleStopWatchState []
//handlers for the stopwatch actions
let handleStart current state =
{state with status = "started"}
//mediator for stopwatch
let StopWatchHandleAction
mapItems
(state:StopWatchState)
(action:StopWatchAction) =
let handler =
mapItems
action
state
action.index
match action.``type`` with
| Start current ->
handler//call handler with state
(fun
(state:SingleStopWatchState)
(action:StopWatchAction) ->
(handleStart current state))
(*
Pausable stopwatch that extends stopwatch and supports
pause action
*)
type PActionType =
| Pause of int
type PausableStopWatchAction = {
``type``:PActionType
index:int
}
type PAction =
| StopWatch of StopWatchAction
| Pausable of PausableStopWatchAction
type SinglePausableStopWatchState = {
status:string
isPaused:bool
}
type PausableStopWatchState = SinglePausableStopWatchState []
//handlers for pausable stopwatch
let handlePause current (state:SinglePausableStopWatchState) =
{state with
status = "paused"
isPaused = true
}
//mediator for pausable stopwatch
let PausableHandleAction
(mapItems : 'a -> 'b [] -> int -> ('b -> 'a -> 'b) -> 'b [])
state
action =
match action with
|Pausable action -> //actions specific to pausable stopwatch
let handler =
mapItems
//warning:This construct causes code to be less generic than indicated by the type annotations. The type variable 'a has been constrained to be type 'PausableStopWatchAction'.
action
state
action.index
match action.``type`` with
| Pause current ->
handler//call handler with state
(fun
state
action ->
(handlePause current state))
| StopWatch action -> //actions from stop watch
let handler =
mapItems
(*
ERROR
This expression was expected to have type
'PausableStopWatchAction'
but here has type
'StopWatchAction'
*)
action
state
action.index
match action.``type`` with
| Start current ->
handler//call handler with state
(fun
state
action -> //would use some of stopwatch handlers here
{state with
status ="started"
})
(*
Application consuming stopwatch and pausable
*)
type ApplicationState = {
stopwatch:StopWatchState
pausablestopwatch:PausableStopWatchState
}
type Action =
| StopWatch of StopWatchAction
| PausableStopWatch of PAction
let ArrayHandler
action
state
index
handlerfn =
state
|> Array.indexed
|> Array.map (
fun (i, item) ->
if index < 0 then
handlerfn item action
else if i = index then
handlerfn item action
else
item)
//application mediator:
let handleAction
(state : ApplicationState)
action =
match action with
| StopWatch
action ->
{state with//return application state
//set the stopwatch state with updated state
// provided by the mediator in stop watch
stopwatch =
StopWatchHandleAction
ArrayHandler state.stopwatch action}
| PausableStopWatch
action ->
{state with//return application state
pausablestopwatch =
PausableHandleAction
ArrayHandler state.pausablestopwatch action}
Function genericity is part of the function declaration. When you pass a function as a value, its genericity is lost.
Consider the following minimal repro:
This program will cause the same warning and same error you're getting. This is because, when I say
f: 'a -> 'a list
, the type variable'a
is a property ofmkTwo
, not property off
. We could make this clearer by declaring it explicitly:This means that, on every given execution of
mkTwo
, there has to be only one'a
. The'a
cannot change during anmkTwo
execution.This has an implication for type inference: the first time the compiler comes across the expression
f 42
, it thinks "hey,f
is called with anint
argument here, so'a
must beint
" - and issues you a helpful warning saying "look, you say this should be generic, but you're actually using it with a concrete typeint
. This construct makes this function less generic than declared".Then, the compiler comes across the expression
f "abc"
. Since the compiler has already decided that'a = int
, and thereforef : int -> int list
, it complains thatstring
is the wrong parameter type.In your original code, the function is
mapItems
, and you're calling it with two different types of arguments: the first time withPausableStopWatchAction
(and get a warning), and the second time withStopWatchAction
(and get an error).There are two general solutions to this problem:
General solution 1: pass the function twice
Here, I pass the exact same function
mkList
both times. In each case the function loses genericity, but it loses it in two different ways: the first time it becomesint -> int list
, and the second time it becomesstring -> string list
. This way,mkTwo
sees it as two different functions, of different types, and so can apply it to different arguments.General solution 2: use an interface
Interface methods, unlike functions, do not lose genericity when the interface is passed as argument. So you can wrap your
mapItems
function in an interface and use it:This is admittedly more bulky than pure functional code, but it gets the job done.
Specific solution for your code
But in your specific case, that is all not even required, because you could "bake" the
action
right intohandlerfn
(here I assume that you're actually usingaction
insidehandlerfn
, even though the code you posted doesn't show that):