2

I wonder if there is a better way of implementing a function that accepts records and modify them.

So I have entities of two types, both have corresponding files on the disk:

type Picture = { Artist : string; File : string }
type Book = { Author : string; File : string }

I want generic function that can copy both pictures and books. In the OOP world I would probably create common interface IArtefact { File : string }, implement it in both records and then create Move method that works on it. Something like:

let move<'a:IArtefact>(a : 'a) (path : string) =
    File.Move(a.File, path)
    { a with File = path }

However I suppose that F# does not support such concept. What is the F# way of doing so?

Alex Netkachov
  • 13,172
  • 6
  • 53
  • 85
  • I don't think there's any important C# feature that isn't supported in F#. Interfaces are supported, as are generics. Whether this is a good way to solve your problem in F# isn't really in the scope of StackOverflow - maybe you'll get more help at CodeReview or Programmers. – Luaan Jan 27 '16 at 13:16
  • As far as I know, there is no way to define record that implements an interface and then cast this record to that interface in F#. This is quite significant "C# feature". However I may be wrong or some other method of doing so exist. So it is completely fine to asking such type of question on SO, IMHO. It is not a code review, nor general programming question. – Alex Netkachov Jan 27 '16 at 13:25
  • Why don't you define the function like the following? `let move (file : string) (path : string)`? Then you can call it with both `aPicture.File` and `aBook.File`. – Mark Seemann Jan 27 '16 at 14:30
  • See also the existing question [F# how to pass equivalent of interface](http://stackoverflow.com/q/34011895/126014) and its answers. – Mark Seemann Jan 27 '16 at 14:31
  • 1
    Oh, I see now that you want to return the value... I'd advise against that, because not only is it impure, but it also violates Command Query Separation, so it quickly becomes difficult to reason about. – Mark Seemann Jan 27 '16 at 14:33
  • Isn't the Command Query Separation a concept of (procedural) imperative programming? "Functional" style seems to be about chaining calls passing immutable local states so returning new record with updated fields seems to fit quite well into that... Anyway, it looks like `{ ... with ... }` cannot be applied to records in generic way so I have to give up with it. – Alex Netkachov Jan 27 '16 at 14:44
  • CQS was defined by Bertrand Meyer, and is an OOD concept. FP is different, but 'real' FP is *pure*, which `File.Move` isn't. That's OK; F# isn't a pure Functional language, but that also means that all the knowledge accrued in the last 60 year about how to structure impure code becomes relevant again - including CQS. – Mark Seemann Jan 27 '16 at 15:02

2 Answers2

4

This is possible, why wouldn't it be ;)

type IArtefact = 
    abstract File: string

type Picture = 
    { Artist : string; File : string }
    interface IArtefact with
        member this.File = this.File

let p = { Artist = "test"; File = "test2" }
(p :> IArtefact).File

Edit: If you want to handle updates:

type IArtefact = 
    abstract File: string
    abstract WithFile: string -> IArtefact

type Picture = 
    { Artist : string; File : string }
    interface IArtefact with
        member this.File = this.File
        member this.WithFile(file) = { this with File = file } :> IArtefact
scrwtp
  • 13,437
  • 2
  • 26
  • 30
  • It is still unclear for me how to define method that returns new artefact with updated file field... `let m<'a when 'a :> IArtefact>(p : 'a) = { p with File = "new" };;` seems close but does not compile... – Alex Netkachov Jan 27 '16 at 13:42
  • 1
    You can't create a "copy" of an arbitrary object when all you know about is interface implementation. The actual object behind the interface could be anything, you can't possibly know how to construct another one of the same kind. – Fyodor Soikin Jan 27 '16 at 14:20
  • I understand that and do not look for such solution. What I'm interested in whether F# has method to do so with its records, nothing more, nothing less. Ideally I would be happy to write `let m<'a when 'a : record >(q:'a):'a = { q }` and that's it. – Alex Netkachov Jan 27 '16 at 14:31
  • @AlexAtNet: No, there's no way to do it - you 'forfeit' all the niceties of record syntax when you cast a record to the interface type. You'd need to add an interface member for handling updates. – scrwtp Jan 27 '16 at 14:49
2

While there is no generic way of change-copying records, there is one for moving anything that has a File:

let inline move from where : string =
    let oldFile = (^T : (member File : string) from)
    do() // move here
    where

type Picture = { Artist: string; File: string }
type Book = { Author: string; File: string }

let p = { Artist = "Vincent van Gogh"; File = "Rooftops" }
let p' = { p with File = move p "The Potato Eaters" }

let b = { Author = "Don Syme"; File = "Generics in CLR" }
let b' = { b with File = move b "Expert F#" }

This could then be expanded to move anything that knows how to be moved:

let inline move' a where =
    let oldFile = (^T : (member File : string) a)
    do() // move here
    (^T: (member moved : string -> ^T) a, where)

type Picture' =
    { Artist: string; File: string } with
    member this.moved where = { this with File = where }

type Book' =
    { Author: string; File: string } with
    member this.moved where = { this with File = where }

let p2 = { Artist = "Vincent van Gogh"; File = "Rooftops" }
let p2' = move' p2 "The Potato Eaters"

let b2 = { Author = "Don Syme"; File = "Generics in CLR" }
let b2' = move' b2 "Expert F#"
CaringDev
  • 8,391
  • 1
  • 24
  • 43
  • This looks acceptable, thank you very much. I think that I will go with something like that. – Alex Netkachov Jan 27 '16 at 15:35
  • 1
    Having something like `{ p with File, Metadata = f() }` where `f : unit -> string * Metadata` in the language would be quite nice as well :) – Alex Netkachov Jan 27 '16 at 15:39
  • Just as an observation, this code mixes metaphors: it uses immutable objects (things with a File member) to represent mutable state (the name of a file on disk). While the code does what you're asking, you could very possibly end up with two picture objects in multithreaded/async code: one with the filename before the move (now invalid) and one after the move. Consider either using a class with a mutable member that protects against simultaneous moves, OR do a copy instead of a move, make the objects IDisposable to delete, and make the caller responsible for making it transactional (yuck). – James Hugard Jan 28 '16 at 15:54
  • @JamesHugard while this is true, it might still make sense to have immutable objects in code, as really preventing 'bad' things to happen is much more difficult: create two instances referring to the same file but only move one, move the file on disk (maybe from an external process), ... That said, I actually tried to find a way to have a constraint for a setter of a mutable File field but didn't succeed. – CaringDev Jan 28 '16 at 16:06