Oldrich Svec
Oldrich Svec

Reputation: 4231

Sub-Records using F#

Assume the following code:

type Large = { v1: int; v2: int }
type Small = { v1: int }

let fn (value: Small) = printfn "%d" value.v1
// or
let fn (value: {v1: int}) = printfn "%d" value.v1

let large = { v1 = 5; v2 = 6 }
fn large

Small is basically a sub-record of Large. fn large throws an error. Is there any "nice" way how to make it work?

Background:

Often there is a situation where you have a lot of data. You want to pass that data to an unknown function provided from the user as a parameter. The only thing that you know is that the function will need a part of the data. But you do not know which part of the data and you do not know about the Small type. You only know about the Large type. One option is to send all the data to the unknown function in the form of a large record. But I believe that the fn function should not accept Large data if it uses only small part of it. The Large data will just make the fn function more difficult to read, understand and unit test. To my opinion the fn function should accept only what it needs and nothing else, but up front I do not know which part of the Large type the fn function will need.

Upvotes: 1

Views: 311

Answers (4)

D.F.F
D.F.F

Reputation: 962

The Large data will just make the fn function more difficult to read, understand and unit test.

I am not sure if I get the problem correctly. But if the main problem is about readability and test-ability, then maybe just use a conversion function

let forgetful (large : Large) = ({v1=large.v1} : Small)

then write your function

let fn (value: Small) = printfn "%d" value.v1

which is readable/testable and get the desire function f by stipulating

let f x = fn (forgetful x)

which doesnt require testing anymore..

Upvotes: 1

Oldrich Svec
Oldrich Svec

Reputation: 4231

Based on the following:

Strange behavior of F# records :

It is important to realize that F# does not use structural typing (meaning that you cannot use record with more fields as an argument to a function that takes record with fewer fields). This might be a useful feature, but it does not fit well with .NET type system. This basically means you cannot expect too fancy things - the argument has to be a record of a well known named record type.

nicolas:

If you truly dont know beforehand which part of Large fn will use, then you have to give it all. this is the only semantically correct choice.

I have come up with two possible solutions:

Solution 1

I basically split the user function into two. The nice thing is that reducedPostprocessFn accepts only what it needs. This function is therefore easy to reason about and unit test. postprocessFn is so short that it is also simple to see what it does. I find this solution similar to active patterns presented by Phillip Trelford. I wonder what the advantage of active patterns is?

(* simulation *)

type Large = {v1: int; v2: int}

let simulation postprocessFn =
  let large = {v1 = 1; v2 = 2}
  postprocessFn large

(* user *)

let reducedPostprocessFn (v1: int) =
  printfn "%d" v1

let postprocessFn (large: Large) =
  reducedPostprocessFn large.v1

simulation postprocessFn

Solution 2

This solution uses duck typing but:

http://msdn.microsoft.com/en-us/library/dd233203.aspx

Explicit Member Constraint: ... not intended for common use.

F# and duck-typing

I just remembered this won't work for record types. Technically their members are fields, although you can amend them with members using with member ....

So I used an ordinary class instead of records. Now I use only one function instead of two but, to be honest, duck typing in F# is just ugly.

(* simulation *)

type Large(v1, v2) =
  member o.v1 = v1
  member o.v2 = v2

let simulation postprocessFn =
  let large = Large(1, 2)
  postprocessFn large

(* user 2 *)

let inline postprocessFn small =
  let v = (^a: (member v1: int) small)
  printfn "%d" v

simulation postprocessFn

Upvotes: 1

Phillip Trelford
Phillip Trelford

Reputation: 6543

Interfaces might be a closer fit to what you have described in the updated question:

type Small =
    abstract v1 : int
type Large =
    inherit Small
    abstract v2 : int

let fn (value: Small) = printfn "%d" value.v1

let large = 
    { new Large with
        member small.v1 = 5
        member large.v2 = 6 }

fn large

With interfaces the Large type can now be composed from 1 or more Small types. Functions can expect only the Small type they need, and the caller can simply pass a Large type to each function.

Depending on the scenario you might also consider having a Large record type that implements one or more Small interfaces.

type Small = abstract v1 : int
type Large = { v1: int; v2: int } with
    interface Small with member this.v1 = this.v1

let fn (value: Small) = printfn "%d" value.v1

let large = { v1 = 5; v2 = 6 }

fn large

Yet another option is to define a union for all the possible Small items, and define Large as a list:

type Item =
    | V1 of int
    | V2 of int

type Large = Item list

let (|Small|) (large:Large) = 
    large |> List.pick (function V1(v1) -> Some(v1)  | _ -> None)

let fn (Small value) = printfn "%d" value

let large = [V1(5);V2(6)]    

fn large

Active patterns can be used to extract Small types from the data. An example of this kind of scheme is the FIX protocol which composes large financial data blocks from small tagged data blocks.

Upvotes: 2

nicolas
nicolas

Reputation: 9805

If you truly dont know beforehand which part of Large fn will use, then you have to give it all. this is the only semantically correct choice.

Now you might be puzzled because you feel that fn does not need all of it, because of the kind of function that fn is, and you are just missing some name to give to that class of functions.

One you give a name to this class, you can reduce Large to only the parts that it needs to access, or break fn in a part that is generic, and another tied to your type for that class of functions, like

type Large = { v1: int; v2: int }
   with x.AdaptToPrintable() =  x.v1.ToString()

and

fn x = printfn "%A" x

PS : I guess I am confused if your problem lies with the semantic or with the operational aspect of specifying values. If the later, then Phil's answer seem to be the best..

Upvotes: 1

Related Questions