Earlier I requested some feedback on my first F# project. Before closing the question because the scope was too large, someone was kind enough to look it over and leave some feedback.
One of the things they mentioned was pointing out that I had a number of regular functions that could be converted to be methods on my datatypes. Dutifully I went through changing things like
let getDecisions hand =
let (/=/) card1 card2 = matchValue card1 = matchValue card2
let canSplit() =
let isPair() =
match hand.Cards with
| card1 :: card2 :: [] when card1 /=/ card2 -> true
| _ -> false
not (hasState Splitting hand) && isPair()
let decisions = [Hit; Stand]
let split = if canSplit() then [Split] else []
let doubleDown = if hasState Initial hand then [DoubleDown] else []
decisions @ split @ doubleDown
to this:
type Hand
// ...stuff...
member hand.GetDecisions =
let (/=/) (c1 : Card) (c2 : Card) = c1.MatchValue = c2.MatchValue
let canSplit() =
let isPair() =
match hand.Cards with
| card1 :: card2 :: [] when card1 /=/ card2 -> true
| _ -> false
not (hand.HasState Splitting) && isPair()
let decisions = [Hit; Stand]
let split = if canSplit() then [Split] else []
let doubleDown = if hand.HasState Initial then [DoubleDown] else []
decisions @ split @ doubleDown
Now, I don’t doubt I’m an idiot, but other than (I’m guessing) making C# interop easier, what did that gain me? Specifically, I found a couple disadvantages, not counting the extra work of conversion (which I won’t count, since I could have done it this way in the first place, I suppose, although that would have made using F# Interactive more of a pain). For one thing, I’m now no longer able to work with function “pipelining” easily. I had to go and change some |> chained |> calls to (some |> chained).Calls etc. Also, it seemed to make my type system dumber–whereas with my original version, my program needed no type annotations, after converting largely to member methods, I got a bunch of errors about lookups being indeterminate at that point, and I had to go and add type annotations (an example of this is in the (/=/) above).
I hope I haven’t come off too dubious, as I appreciate the advice I received, and writing idiomatic code is important to me. I’m just curious why the idiom is the way it is 🙂
Thanks!
An advantage of members is Intellisense and other tooling that makes members discoverable. When a user wants to explore an object
foo, they can typefoo.and get a list of the methods on the type. Members also ‘scale’ easier, in the sense that you don’t end up with dozens of names floating around at the top level; as program size grows, you need more names to only be available when qualified (someObj.Method or SomeNamespace.Type or SomeModule.func, rather than just Method/Type/func ‘floating free’).As you’ve seen, there are disadvantages as well; type inference is especially notable (you need to know the type of
xa priori to callx.Something); in the case of types and functionality that is used very commonly, it may be useful to provide both members and a module of functions, to have the benefits of both (this is e.g. what happens for common data types in FSharp.Core).These are typical trade-offs of “scripting convenience” versus “software engineering scale”. Personally I always lean towards the latter.