mamcx
mamcx

Reputation: 16196

How encode in a dictionary multiple functions of different types

I'm building a interpreter in F#. I'm trying to be clever and generalize the interpretation of primitive operators as function calls (in this case, as reductions).

This is the idea:

let reduce fx values =
    Array.reduce values fx

let primitivesEnv = 
    let r = Dictionary<string,'T -> 'T -> 'T>()
    r.["int_+"] <- reduce (fun(l:int, r:int) -> l + r)
    r.["int_-"] <- reduce (fun(l:int, r:int) -> l - r)
    r

So I could later do this:

env.["int_+"]([| 1, 2 |])

Of course, the type-checker reject this with

Warning FS0064: This construct causes code to be less generic than indicated by the type annotations. The type variable 'T has been constrained to be type ''a -> 'a -> 'a'. Error FS0001: Type mismatch. Expecting a ('a -> 'a -> 'a) -> ('a -> 'a -> 'a) -> 'a -> 'a -> 'a but given a ('a -> 'a -> 'a) -> 'a The resulting type would be infinite when unifying ''a' and '('a -> 'a -> 'a) -> 'a -> 'a -> 'a'

P.D: I know how do this as a simple interpreter, but trying to build a solution that let me build dozens of methods in a generic way, without make a MATCH for each one.

Upvotes: 1

Views: 169

Answers (1)

TheInnerLight
TheInnerLight

Reputation: 12184

First of all, there are some issues with your code. You have added a type annotation to your dictionary of 'T -> 'T -> 'T but it seems to me that it should return a function of type 'T[] -> 'T since you are trying to give it an array and evaluate a reduction.

Also, [| 1, 2 |] is an array of one tuple, you need an array of multiple values, such as this one : [| 1; 2 |]

I fixed your code like this:

let reduce values =
    Array.reduce values

let primitivesEnv = 
    let r = System.Collections.Generic.Dictionary<string,'T[] ->'T>()
    r.["int_+"] <- reduce ((+) : int -> int -> int)
    r.["int_-"] <- reduce ((-) : int -> int -> int)
    r

let result = primitivesEnv.["int_+"] [| 1; 2 |]

Unfortunately, this isn't the end of the problems.

We might have said that the dictionary is of type 'T[] ->'T but it isn't, the only valid type for the dictionary is int[] -> int, the first int -> int -> int type annotation creates that constraint. If we leave out that type annotation, 'T is constrained to int when we use it with an int[].

The type parameter 'T must always be resolved to a fixed type in some way, it isn't a wild card that lets you use anything.

This means that the dictionary approach is fine until you want to add float (or some other type) in addition to just int. Your only option, when using a dictionary, is to either choose one type or throw away some of your type-safety and resolve the specific type at runtime.

The simplest change to do that would be to create some union cases to describe the different reduction types:

type Reduction =
    |IntReduction of (int[] -> int)
    |FloatReduction of (float[] -> float)

You then create a Dictionary<string, Reduction> instead of a Dictionary<string,'T[] ->'T>.


When it comes to the more general problem of creating an interpreter in F#, I would start by creating a structured set of discriminated unions to describe the expressions and structure of the mini-language you wish to interpret, this is called an Abstract Syntax Tree (AST).

You can then define a run function that walks the tree and performs all the computations described by the AST.

I would also use a parser combinator library (I'd recommend FParsec) to parse whatever structured text into an abstract syntax tree of the union cases you defined in the step above.

Phillip Trelford has an example online of how to do this using FParsec for a simple C# AST: http://www.fssnip.net/lf This is probably far more powerful than what you need but hopefully it'll give you a starting point.

Upvotes: 1

Related Questions