7

I often have a function with multiple parameters of the same type, and sometimes use them in the wrong order. As a simple example

let combinePath (path : string) (fileName : string) = ...

It seems to me that phantom types would be a good way to catch any mix ups. But I don't understand how to apply the example in the only F# phantom types question.

How would I implement phantom types in this example? How would I call combinePath? Or am I missing a simpler solution to the problem?

Community
  • 1
  • 1
Fsharp Pete
  • 741
  • 4
  • 16

2 Answers2

12

I think the easiest way is to use discriminated unions:

type Path = Path of string
type Fname = Fname of string
let combinePath (Path(p)) (Fname(file)) = System.IO.Path.Combine(p, file)

You could call it this way

combinePath (Path(@"C:\temp")) (Fname("temp.txt"))
Petr
  • 4,280
  • 1
  • 19
  • 15
  • 5
    Since those are one-case DUs, you can also do `let combinePath (Path(p)) (Fname(f)) = ...`, the end-result will be the same :) – Patryk Ćwiek Sep 29 '14 at 14:13
  • That would mean declaring a type for each parameter type. I guess I was thinking it could be sort of like units of measure, string string. Though it will still need types for Path and FileName that way too, so it's all the same in the end... answering my own point I think. – Fsharp Pete Sep 29 '14 at 14:40
  • 1
    @Fsharp Pete: I think these params are of different string types anyway. If you are not declare them your program is prone to errors (for example file name string cannot contain symbol '\' but path string can) Declaring them explicitly you can add validation etc. Take a look at good resource: http://fsharpforfunandprofit.com/posts/designing-with-types-single-case-dus/ – Petr Sep 29 '14 at 15:00
8

I agree with Petr's take, but for completeness, note that you can only use phantom types when you've got a generic type to use them with so you can't do anything with plain string inputs. Instead, you could do something like this:

type safeString<'a> = { value : string }

type Path = class end
type FileName = class end

let combinePath (path:safeString<Path>) (filename:safeString<FileName>) = ...

let myPath : safeString<Path> = { value = "C:\\SomeDir\\" }
let myFile : safeString<FileName> = { value = "MyDocument.txt" }

// works
let combined = combinePath myPath myFile

// compile-time failure
let combined' = combinePath myFile myPath
kvb
  • 54,864
  • 2
  • 91
  • 133
  • Clearly the discriminated union is the better way of doing this. Phantom types seem redundant for F#. I guess that's why there isn't much coverage of it. – Fsharp Pete Sep 30 '14 at 11:56
  • @FsharpPete - I don't think that's quite right; in this particular scenario DUs may be better, but there are certainly scenarios where phantom types are useful (and I don't think F# is notably different from most other languages in terms of when to use one technique or the other). – kvb Sep 30 '14 at 14:43
  • Yes, I see it now. With the phantom types approach you can specify the phantom type from outside the module/package not so with the discriminated union. – Fsharp Pete Oct 14 '14 at 11:44