Search code examples
oopf#functional-programmingintellisensepurely-functional

Should I write code that takes advantage of Intellisense?


I am starting with F# and made some progress in understanding the syntax. However, I remain unclear about the best way to use F#'s features. In Python, where I am coming from, there is usually one "best" (almost canonical) way of doing things. Maybe that is also the case for F#, but I have not figured it out. So my questions below are about the best way to use F#, and not technical questions about F#'s syntax.

Recently I saw a video by Dr. Eric Meijer (C9 Lectures - Functional Programming Fundamentals Chapter 2 of 13) in which Dr. Meijer praises the dot notation of OOP, observing that it allows Intellisense to display a list of available methods. He laments that such a facility is not available in pure FP, which makes programming so much easier by helping programmers "move forward."

A bit of experimenting showed that of course Intellisense works with F# classes, but also works with F# records, which, like classes, use the dot notation. This means one can shape one's code so as to take advantage of Intellisense without going all the way to write classes (I am assuming that in F# classes are heavier and slower than records, please correct me if I am wrong).

The following code shows two ways of writing code (call them "versions") that perform the same operations:

// Create a record type with two values that are functions of two arguments
type AddSub = {add2: int -> int -> int; sub2: int -> int -> int}

// Instantiate a record
let addsub a =
    {add2 = (fun x y -> a + x + y); sub2 = (fun x y -> a - x - y)}

// Returns 7, Intellisense works on (addsub 0).
(addsub 0).add2 3 4
// Returns 3, Intellisense works on (addsub 10).  
(addsub 10).sub2 3 4

// Create two functions of three arguments
let add3 a x y = a + x + y
let sub3 a x y = a - x - y

// Also got 7, no Intellisense facility here
add3 0 3 4
// Also got 3, no Intellisense facility here
sub3 10 3 4

This shows that there is an intermediate strategy between pure FP and OOP: creating record types with function values, as above. Such a strategy organizes my code in meaningful units centered about objects (record instances) and allows me to use Intellisense, but lacks some of the features provided by classes, like inheritance and subclass polymorphism (again correct me if I am wrong here).

Coming from an OOP background I feel that if an object like a in the code above is somehow more "significant" (I will leave that term undefined) than the parameters x and y such a coding strategy would be justified, both on the grounds of code organization and of the ability to use Intellisense. On the other hand, burned by the complexities of OOP, I would rather remain in the "pure" FP realm.

Is the use of records a worthwhile compromise between the two extreme alternatives (OOP and pure FP)?

In general, given three alternatives (pure FP, records as above, or classes) what are general guidelines on the circumstances under which one alternative is preferred over the others?

Finally, are there other coding strategies available that would help me organize my code and/or take advantage of Intellisense?


Solution

  • Intellisense still works just fine in F#, but at a module level rather than at a class level. I.e., I just typed in List. and once I typed the dot, VS Code (with the Ionide plugin providing F# Intellisense) gave me a list of possible completions: append, average, averageBy, choose, chunkBySize...

    To get that benefit from your own functions, put them in a module:

    module AddSub =
        let add2 x y = x + y
        let sub2 x y = x - y
        let add3 a x y = a + x + y
        let sub3 a x y = a - x - y
    

    Now when you type AddSub., after you type the dot, Intellisense will suggest add2, add3, sub2, and sub3 as possible followups. Yet you've kept your functions "clean" and curryable, in "proper" F# style.

    Finally, one other piece of advice about function design. You mentioned having functions in which one parameter (like a in the add3 and sub3 functions) is somehow more "significant" than the other parameters. In F#, any such parameter should probably be the last parameter, because that allows you to put it in a function chain using the |> operator, like so:

    let a = 20
    a |> AddSub.add3 5 10 |> AddSub.sub3 2 3  // Result: 30
    

    Or rather, using the style that most people prefer when there's a "pipeline" of operations from a single starting value:

    let a = 20
    a
    |> AddSub.add3 5 10
    |> AddSub.sub3 2 3
    // Result: 30
    

    Lining up the pipeline vertically becomes more important when there are more operations in it. My rule of thumb is that if there are more than two total "extra" parameters specified in the pipeline (the pipeline above has four "extra" parameters, two each for the add3 and sub3 functions), or if any of the "extra" parameters is more complicated than a single value (e.g., if one parameter is an anonymous function like (fun x -> sprintf "The value of x was %d" x) or some such), then you should arrange it vertically.

    P.S. If you haven't read it yet, read Scott Wlaschin's excellent series on Thinking Functionally. It'll help explain many things about this answer, like why I suggested putting the "most significant" argument last. If you didn't immediately understand my brief comment about how that enables you to use it with the |> parameter, or if there was anything else that puzzled you about this answer, then you'll probably get a lot of benefit out of Scott's articles.