Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write a list of (string*obj) without using upcast

Tags:

f#

dsl

My goal is to write list of name/value pairs in a DSL and make this readable. The values here can be int, float, string or a list of any of those types.

I am using string * obj pairs and passing them to a function that takes a (string * obj) list.

Is there to write the list without upcasting the obj parameter?.

let myfun (values:(string*obj) list) =
    // Do something...

// This is pretty ugly
myfun ["Name", upcast "Freddie"; "Age", upcast 50]

// This would be the ideal
myfun ["Name", "Freddie"; "Age", 50]
like image 861
Sean Kearon Avatar asked Feb 04 '17 17:02

Sean Kearon


2 Answers

Programming 101: if you find yourself repeating the same thing over and over, package it up for reuse, make it a function. In your case, the function would be generic (i.e. take parameter of any type) and do the upcast on the parameter:

let pair name value = name, value :> obj
myfun [pair "Name" "Freddie"; pair "Age" 50]

Hmm... Not much nicer, is it? But wait, we're not done yet! Now that you have this function, you can give it a nicer name, which would make calling it nicer. Say, ==>:

let (==>) name value = name, value :> obj
myfun ["Name" ==> "Freddie";  "Age" ==> 50]

If your set of possible types is known in advance and relatively small (as your question seems to indicate), you can go a step further and have the compiler check that only allowed types are used. To do this, you'll need to use method overloads, statically resolved type constraints and some syntactic trickery:

type Casters() =
  static member cast (v: string) = v :> obj
  static member cast (v: float) = v :> obj
  static member cast (v: int) = v :> obj
  static member cast (v: string list) = v :> obj
  static member cast (v: float list) = v :> obj
  static member cast (v: int list) = v :> obj

let inline cast (casters: ^c) (value: ^t) =
    ( (^c or ^t) : (static member cast : ^t -> obj) value)

let inline (==>) name value = name, (cast (Casters()) value)

["Name" ==> "Freddie"; "Age" ==> 50]  // OK
["What?" ==> true]  // Error: "bool" is not an allowed type
like image 83
Fyodor Soikin Avatar answered Nov 15 '22 11:11

Fyodor Soikin


Your say that your values can only have certain listed types. I wonder if you have a particular reason to use obj instead of a discriminated union, which is perfectly suited for this task?

I've modified Fyodor's answer to use a DU type instead of obj:

type Value =
    | Int     of int      | Float     of float      | String     of string
    | IntList of int list | FloatList of float list | StringList of string list

type Casters() =
  static member cast v = Int v
  static member cast v = Float v
  static member cast v = String v
  static member cast v = IntList v
  static member cast v = FloatList v
  static member cast v = StringList v

let inline cast (casters: ^c) (value: ^t) =
    ( (^c or ^t) : (static member cast : ^t -> Value) value)

let inline (==>) name value = name, (cast (Casters()) value)

["Name" ==> "Freddie"; "Age" ==> 50]  // OK
["What?" ==> true]  // Error: "bool" is not an allowed type

The advantage of this approach is that you now have type-checked pattern matching when you access the values, and you don't have to do unsafe downcasting of the obj:

let myfun (values:(string*Value) list) =
    values
    |> List.map (fun (k, v) ->
        match v with
        | Int v -> k  + ":" + string v
        | String v -> k + ":" + v.Trim() )
        // etc.
    |> String.concat "\n"

myfun ["Name" ==> "Freddie"; "Age" ==> 50] |> printfn "%s"
//Name:Freddie
//Age:50
like image 34
TheQuickBrownFox Avatar answered Nov 15 '22 12:11

TheQuickBrownFox