Type inference not working when passing map function

后端 未结 1 860
夕颜
夕颜 2020-12-06 23:38

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.

W

相关标签:
1条回答
  • 2020-12-07 00:28

    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:

    let mkList x = [x]
    let mkTwo (f: 'a -> 'a list) = (f 42), (f "abc")
    let two = mkTwo mkList
    

    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 of mkTwo, not property of f. We could make this clearer by declaring it explicitly:

    let mkTwo<'a> (f: 'a -> 'a list) = (f 42), (f "abc")
    

    This means that, on every given execution of mkTwo, there has to be only one 'a. The 'a cannot change during an mkTwo 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 an int argument here, so 'a must be int" - and issues you a helpful warning saying "look, you say this should be generic, but you're actually using it with a concrete type int. 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 therefore f : int -> int list, it complains that string 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 with PausableStopWatchAction (and get a warning), and the second time with StopWatchAction (and get an error).

    There are two general solutions to this problem:

    General solution 1: pass the function twice

    let mkList x = [x]
    let mkTwo f g = (f 42), (g "abc")
    let two = mkTwo mkList mkList
    

    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 becomes int -> int list, and the second time it becomes string -> 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:

    type MkList =
        abstract member mkList : 'a -> 'a list
    
    let mkList = { new MkList with member this.mkList x = [x] }
    let mkTwo (f: MkList) = (f.mkList 42), (f.mkList "abc")
    let two = mkTwo mkList
    

    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 into handlerfn (here I assume that you're actually using action inside handlerfn, even though the code you posted doesn't show that):

    let mapItems
      state 
      index
      handlerfn =
        state
          |> Array.indexed
          |> Array.map (
            fun (i, item) ->
              if index < 0 then
                handlerfn item 
              else if i = index then
                handlerfn item 
              else
                item)
    
    ...
    
    let handleAction
      (mapItems : 'a [] -> int -> ('a -> 'a) -> 'a [])
      state
      action =
        match action with
        |Pausable action -> //actions specific to pausable stopwatch
            let handler = 
              mapItems
                state
                action.index
            match action.``type`` with
             | Pause current ->
                 handler//call handler with state
                   (fun state ->
                         (handlePause current state))
        | StopWatch action -> //actions from stop watch
           let handler = 
             mapItems
               state
               action.index
           match action.``type`` with
             | Start current ->
                 handler//call handler with state
                   (fun state ->
                       //would use some of stopwatch handlers here
                       {state with
                         status ="started"
                       })
    
    0 讨论(0)
提交回复
热议问题