1

After more than 10 years of object oriented programming with C#, when learning about F# I am struggling in imagining how to design applications in a modular way, that allows to add functionality without modify code that is already there.

How would you dynamically add cases to a discriminated union?

How do you work dividing code in assemblies when the order in files matter?

How for example a strategy pattern fits into F#?

vtortola
  • 34,709
  • 29
  • 161
  • 263
  • 3
    I like the question but it is too broad. Please focus on one question and show some code you have. That will give us better context. The general F# patterns question should be an aside rather than the "meat" of the post. – Paul Sasik Oct 29 '14 at 14:20
  • this question just don't fit SOs philosophy - it's to broad and to opinionated - for example you cannot "dynamically" (if it means outside it's definition) extent DU and this has something to do with the [expression problem](https://en.wikipedia.org/wiki/Expression_Problem) - which does lead to a very long discussion – Random Dev Oct 29 '14 at 14:21
  • as for how to divide: I don't - I collect data/functions by *domain* and but them into their own file(s)/module(s) - I never had a problem with the ordering - it just makes you think more about a problem when you face a mutual recursive definition across files/namespaces/assemblies (a code smell IMO) – Random Dev Oct 29 '14 at 14:23
  • the strategy patter is just not needed at all using DU (maybe you could say it's right there inside `match` and your data-structure) – Random Dev Oct 29 '14 at 14:24
  • If you can't know all of the possible options at compile time, you can't benefit from much of the safety that F# provides. – Christopher Stevenson Oct 29 '14 at 14:37
  • Sorry the question is a little bit broad. Not knowing all the possible options at compile time is the strength of the extensibility of many of the projects where I work at. How would you achieve the extensibility of ASP.NET MVC on F#? That is what I am curious about. – vtortola Oct 29 '14 at 14:41
  • Extensibility is done with functions, rather than objects or interfaces, in functional programming. The abstraction mechanism is the functions' signatures. So a particular extension of a framework becomes a composition of functions--rather like putting together tools--most of them part of the framework you're using, and some of them your extended functionality. – Christopher Stevenson Oct 29 '14 at 14:51

4 Answers4

3

The question has five parts, some of which have not been answered yet.

...design patterns in F# (in general): You can implement the well-known formal design patterns in F# just as you would in C# or VB.NET (GOF design patterns, enterprise architecture patterns, cloud design patterns, etc.). In some cases, implementing a formal design pattern is not necessary in F#, because the language has built-in features who make the pattern obsolete. An often-cited example in this respect is the visitor pattern. However, replacing a formal design pattern with a functional implementation is also possible in C# or VB.NET (but less elegant). On the other hand, functional languages have their own "patterns", but they're usually not called "patterns" as they tend to be just algorithmic approaches, which are less formalized.

...allow to add functionality without modifying code that is already there -> See @kvb's answer.

...dynamically add cases -> See @kvb's answer.

...dividing code in assemblies when the order in files matter: This restriction helps write code whose path is simple to understand. Mutual dependencies, which are practically encouraged in C#/VB.NET, raise complexity and "spaghettify" the code. However, in some cases, mutual dependency-like constructs are legitimate. You can define them in F# in four ways:

  1. Use a class type. Members of the same class can mutually call each other regardless of the order of their declarations.
  2. Use the and keyword to make functions, values, or types (in the same file) mutually dependent.
  3. Use intrinsic type extensions to define parts of a type before and after you define other types (in the same file)
  4. Use the Separated Interface pattern to break the mutual dependencies.

...strategy pattern -> See also @plinth's answer. You can do it in the GOF way like in C# or VB.NET. You can also do it in a functional way in either F#, C#, or VB.NET (they are all functional languages). F#, however, is more succinct/expressive and has additional functional features such as function combination, partial function application, etc.:

let stratAdd x y = x + y
let stratMul x y = x * y

let partialStratAdd = stratAdd 10
let partialStratMul = stratMul 20

let chosenStrat = 
    if Random().Next(1, 100) < 50 then partialStratAdd 
    else partialStratMul

chosenStrat 7 // Gets either 17 or 140
Marc Sigrist
  • 3,964
  • 3
  • 22
  • 23
2

Since functions are manipulatable and composable in F#, the strategy pattern is ubiquitous.

For example, suppose I have a type definition for a point and path elements:

type Point = { X:float; Y:float }
type PathOp =
| Close
| MoveTo of Point
| LineTo of Point
| CurveTo of Point * Point * Point

and function types:

type PointMutator = Point->Point
type PathOpMutator = PathOp->PathOp

NB: in this context a mutator is a function that given an object returns a new object of the same type with the contents changed in some way (maybe). I can write an object to change elements in a sequence of paths like this:

let pathMutator (mutator:PathOpMutator) path =
    path |> Seq.map(mutator)

Which creates a new sequence of path operations creating mutations of each one.

Now, I can do this:

let pathPointMutator (mutator:PointMutator) op =
match op with
| Close -> Close
| MoveTo(pt) -> MoveTo(mutator(pt))
| LineTo(pt) -> LineTo(mutator(pt))
| CurveTo(cp1, cp2, dp) -> CurveTo(mutator(cp1), mutator(cp2), mutator(cp3))

So if I wanted to write code to offset the entire contents of a path, I would visit each element of the path applying an offset strategy:

let pointOffseter offset pt =
    { X = offset.X + pt.X; Y = offset.Y + pt.Y }

let offsetPath offset = pathMutator (pathPointMutator (pointOffsetter offset))

Now, I'm heavily using partial function application here - when I pass only offset into pointOffsetter, I'm getting back a function of Point->Point which will add the bound offset to its argument. This function is in turn partially applied to pathPointMutator which returns a function of PathOp->PathOp which is partially applied to pathMutator returning a function of Seq<PathOp>->Seq<PathOp>.

Essentially, what I've done is created a means of applying a strategy to each path operation in order to create new path operations.

Rather than using an interface to define the strategy, I'm instead using a strongly typed function to define the strategy. The concept is the same but the concrete implementation is different.

If you want to interoperate with other .NET languages you can do this with interfaces if you really want:

type ICalculate =
    abstract member Calculate : float->float->float

type Adder =
    interface ICalculate with
        member this.Calculate x y = x + y

type Subber =
    interface ICalculate with
        member this.Calculate x y = x - y

type Responder =
    member private this.GetCalculator(op) =
    match op with
    | '+' = new Adder()
    | '-' = new Subber()
    | _ -> ivalidArg "op" "op not defined"
    member this.Respond op x y =
        let calc = GetCalculator(op)
        calc x y

This is, of course, way too much work just to do calculations, but your focus should be on the blind usage of an ICalculator without knowing it's implementation details.

Now, to be more precise in the strategy definition, Responder is more accurate in that the actual strategy is selected late, whereas in the PathOp example, it's done a priori. This is one of the things in the GoF patterns that can be aggravating if you're trying to be precise - many of the patterns overlap and your actual implementation may not have a solid taxonomy.

plinth
  • 48,267
  • 11
  • 78
  • 120
  • I more or less understand your approach, but then your F# code would not be extensible, right? There is no way you can add newcoptions to a DU without alter the DU itself. – vtortola Oct 30 '14 at 15:55
  • This has nothing to do with adding to a DU. As others have stated, DU's are static (and therefore compile-time type safe). – plinth Oct 30 '14 at 16:01
  • Adding cases to a DU was one of the questions since I was concerned about extensibility. – vtortola Oct 30 '14 at 16:41
  • *shrug* you asked 3 questions, I answered 1. – plinth Oct 30 '14 at 17:34
  • Sorry, I am very grateful for your answer. I was just curious about the extensibility point. Cheers. – vtortola Oct 30 '14 at 18:09
1

The question is kind of general but I could try to answer to more concrete questions:

  1. It is very helpful that the set of possible cases of discriminated unions is known at compile-time, so the compiler can check when your code doesn't handle a possible case.

  2. There is a great resource that discuss the design of F# program I think it could answers your question: http://fsharpforfunandprofit.com/posts/recipe-part3/

  3. Good answer(s) could be found here: Strategy pattern in F#

Community
  • 1
  • 1
Petr
  • 4,280
  • 1
  • 19
  • 15
  • Take a look at [this link from programmers.stackexchange.com](http://programmers.stackexchange.com/questions/209357/from-a-high-level-programming-perspective-where-does-the-different-paradigm-b). – Christopher Stevenson Oct 29 '14 at 14:30
1

If you know that you'll need to extend a set of choices in a flexible way, then a discriminated union is probably not the right way to model your problem. Instead, discriminated unions are most useful when the possible universe of choices is known up front (e.g. you can model a list as either empty or as having a first item plus a remainder, and that model will never need to change).

If you want to extend your design in unanticipated ways, then using C#-like type hierarchies (or at least interfaces) is often a very reasonable approach even in F#. In some cases, simpler approaches like records of functions can also work very well (and there's actually a fairly deep connection between functions and objects - see e.g. http://c2.com/cgi/wiki?ClosuresAndObjectsAreEquivalent and http://people.csail.mit.edu/gregs/ll1-discuss-archive-html/msg03277.html, to which it refers).

Finally, I wouldn't advocate it, but there is a way to use discriminated unions in a fairly intricate scheme to solve the "expression problem"; see Vesa Karvonen's post here: http://lambda-the-ultimate.org/node/2232#comment-31278 (though if you're new to ML-ish languages it may be incomprehensible).

kvb
  • 54,864
  • 2
  • 91
  • 133