Contents
In other words, the "abstract." Actually, this is fairly long in and of itself!
Can we use just the native .NET classes for developing code, rather than immediately writing an application specific class that often is little more than a container? Can we do this using aliases, a fluent style, and extension methods? If we're going to just use .NET classes, we're going to end up using generic dictionaries, tuples, and lists, which gets unwieldy very quickly. We can alias these types with using
statements, but this means copying these using statements into every .cs file where we want to use the alias. A fluent ("dot-style") notation reduces code lines by representing code in a "workflow-style" notation. In C#, if we don't write classes with member methods, then we have to implement behaviors as extensions methods. Using aliases improves semantic readability at one level at the cost of confusing generic type nesting in the alias definition. Extension methods can be taken too far, resulting in two rules: write lower level functions for semantic expressiveness, and avoid nested parens that require the programmer to maintain a mental "stack" of the workflow. In contrast to C#'s using
aliases, F# type definitions are not aliases, they are concrete types. New type definitions can be created from existing types. Type definitions can also be used to specify a function's parameters and return value. The forward pipe operator |>
is similar to the fluent "dot" notation in C#, but the value on the left of the |>
operator "populates" the last parameter in the function's parameter list. When functions are written that return something, the last function must be piped to the ignore function, which is slightly
awkward. F# type dependencies are based on the order of the files in the project, so a type must be defined before you use it. In C#, creating more complex aliases get messy real fast -- this is an experiment, not a recommendation for coding practices! In F#, we don't need an Action or Func class for passing functions because F# inherently supports type definitions that declare a function's parameters and return value -- in other words, everything in functional programming is actually a function. Tuples are a class in C# but native to functional programming, though C# 6.0 makes using tuples very similar to F#. While C# allows function parameters to be null, in F#, you have to pass in an actual function, even if the function does nothing. F# uses a nominal ("by name") as opposed to structural inference engine, Giving types semantically meaningful names is very important so that the type inference engine can infer the correct type. In C#, changing the members of class doesn't affect the class type. Not so with F# (at least with vanilla records) -- changing the structure of a record changes the record's type. Changing the members of a C# class can, among other things, lead to incorrect initialization and usage. Inheritance, particularly in conjunction with mutable fields, can result in behaviors with implicit understanding like "this will never happen" to suddenly break. Extension methods and overloading creates semantic ambiguity. Overloading is actually not supported in F# - functions must have semantically different names, not just different types or parameters lists. Object oriented programming and functional programming both have their pros and cons, with some hopefully concrete discussion presented here.
Summary: Can we use just the native .NET classes for developing code, rather than immediately writing an application specific class that often is little more than a container? Can we do this using aliases, a fluent style, and extension methods?
Sometimes I ask myself questions intended to challenge my fundamental concepts programming. The question I recently asked myself regarding C# coding is:
- Do I really need all those classes?
Obviously there are lots of programming languages where you don't necessarily use classes, like Javascript, where classes are "primarily syntactical sugar over JavaScript's existing prototype-based inheritance." (reference) My basis for this question is that whenever I start coding something in C#, the first thing I do is write class Foo
or static class Foo
. C# enforces this behavior -- everything, even extension methods, needs to be wrapped in a class. So the question immediately evolved into:
- When writing in C#, do I really need to implement specialized classes that are containers for data and behaviors?
I decided to answer that question by re-implementing three of the core components that I use in most of my coding:
- A service manager
- A thread pool (a service)
- A semantic publisher/subscriber (another service)
without using any classes other than what the .NET framework provides. (Don't ask why I use a thread pool instead of Task
et al -- the answer is outside of the scope of this article, so just go with it, even if you squawk at the implementation.) C# (with .NET 4.7) has a rich syntax to leverage the existing .NET framework classes, so let's look at what an implementation without writing any custom classes except the container class for extension methods.
(that's a "side note") -- A secondary aspect of this question is to determine whether code can be written such that it is easier to understand. I'm a firm believer that the less code you have to read to understand what the code is doing, the more easily understood it becomes. We'll see how it goes.
So, how we go about doing this? And why does this lead us into looking at F#?
Summary: If we're going to just use .NET classes, we're going to end up using generic dictionaries, tuples, and lists, which gets unwieldy very quickly. We can alias these types with using
statements, but this means copying these using statements into every .cs file where we want to use the alias.
One of the conveniences of writing a class is that it expresses a strongly typed alias of something else. A service manager, for example, needs some way of mapping the service "name" to the service implementation. The service "name" is a somewhat abstract concept. For example, it could be a string literal, a type (like an interface), a type, or an enumeration. String literals are a bad idea because of spelling errors and case sensitivity, and enumerations are a bad idea because the numeric value associated with the enumeration name could change. Interfaces are a bad idea because the service manager would need a reference to the assembly or assemblies defining the interface. So, we'll go with a map of types, and we'll use a ConcurrentDictionary
because I want the service manager to be thread safe and I don't want to deal with thread safety myself since .NET provides a nice thread safe dictionary:
ConcurrentDictionary<Type, Object>
The service implementation is an Object
, which I'm not to thrilled with but we'll go with that because I don't want to "pollute" my code with interfaces and type constraints.
Having to write ConcurrentDictionary<Type, Object>
anywhere we need to pass in the service manager, like this:
void NeedsAService(ConcurrentDictionary<Type, Object> serviceManager)
is unwieldy, ugly, and doesn't convey anything meaningful in the type. Yuck. But we can create an alias instead:
using Services = System.Collections.Concurrent.ConcurrentDictionary<System.Type, object>;
Something interesting here - note that I'm not calling this a "service manager" -- it's a collection of service types that map to their implementations, so I call it "Services."
The drawback with this approach is that the alias needs to be defined in every .cs file that uses this type. Another yuck, but we'll go with it, as I can now write:
void NeedsAService(Services serviceManager)
In C#, using aliases to .NET's classes defeats the OOP tenet of encapsulation -- the aliased object can be directly manipulated by the caller, who can delete items, make the wrong key-value associations, and so forth. Ironically, if implemented in F# as immutable dictionaries, tuples, and collections, this becomes a non-issue because manipulating a collection or dictionary results in copy of the dictionary or collection (or at best, a collection that shares the tail portion but can have different heads) -- but this approach is beyond the scope of this article.
Summary: A fluent ("dot-style") notation reduces code lines by representing code in a "workflow-style" notation.
The other tenet of what I want to explore as "minimal coding" is a "fluent" (dot notation) style of programming. Why? Because this results in a very minimal and linear syntax for doing things. For example (which I'll expound upon later):
new Thread(new ThreadStart(() => aThread.Forever(t => t.WaitForWork().GetWork().DoWork()))).IsBackground().StartThread();
The typical implementation would look like this:
private void ThreadHandler()
{
while (true)
{
WaitForWork();
var work = GetWork();
DoWork(work);
}
}
var thread = new Thread(new ThreadStart(ThreadHandler));
thread.IsBackground = true;
thread.Start();
Remember, I'm not debating the pros and cons of the two approaches, I'm exploring what a minimal programming style would look like, and a fluent notation is part of that minimal approach, at least at the higher level of abstraction of "I, as user of my library code."
Summary: In C#, if we don't write classes with member methods, then we have to implement behaviors as extensions methods.
If we're not wrapping behaviors within classes (and remember, those classes are little more than aliases for a more general purpose container) then how do we implement semantic behaviors? With extension methods! At least with regards to C#, this inverts the whole programming model. Instead of:
class ASemanticAliasOf
{
SomeCollection collection;
void SomeOperationA() {...}
void SomeOperationB() {...}
}
we have instead:
static void SomeOperationA(this SomeCollection collection) {...}
static void SomeOperationB(this SomeCollection collection) {...}
Of course, extension methods need to be wrapped in a static class
. Can't get away from that!
Summary: Using aliases improves semantic readability at one level at the cost of confusing generic type nesting in the alias definition. Extension methods can be taken too far, resulting in two rules: write lower level functions for semantic expressiveness, and avoid nested parens that require the programmer to maintain a mental "stack" of the workflow.
A dictionary is just a dictionary, it knows nothing about being a service manager, and while we could use it directly in our code, we lose a lot of semantic meaning when we code a key-value assignment:
serviceManager[typeof(MyService)] = myService;
Other useful behaviors are lost as well, for example:
- Is
myService
actually of type MyService
? - Is
MyService
already assigned to a service? - Is myService null?
To mitigate this, the service manager has a few extension methods to improve the semantics of using the dictionary. First though, we'll define the type aliases:
using Service = System.Object;
using Services = System.Collections.Concurrent.ConcurrentDictionary<System.Type, System.Object>;
Notice that I'm mapping to an object rather than an interface that declares that the instance "is a service type." This is more to avoid cluttering the examples, particularly the F# examples, with unnecessary types. It's also a moot point in the C# implementation because getting a service from its type casts the object back to that type, so we're all good (famous last words.)
Notice that with C#, we can't use aliases in other aliases. In other words, we can't say:
using Services = System.Collections.Concurrent.ConcurrentDictionary<System.Type, Service>;
Also notice the explicit "namespace-dot-class" notation. We also can't do something like this:
using Services = ConcurrentDictionary<Type, Service>;
That's a drawback to using aliases in C#, as the alias notation can get ugly very very quickly -- we'll see this later.
We'll implement the minimal extension methods to provide better semantics for the operations of a service manager:
public class ServiceManagerException : Exception
{
public ServiceManagerException(string msg) : base(msg) { }
}
public static class ServiceManagerExtensionMethods
{
public static Services Register<T>(this Services services, T service)
{
services.ShouldNotContain<T>()[typeof(T)] = service;
return services;
}
public static T Get<T>(this Services services)
{
return (T)services.ShouldContain<T>()[typeof(T)];
}
private static Services ShouldNotContain<T>(this Services services)
{
if (services.ContainsKey(typeof(T)))
{
throw new ServiceManagerException(String.Format("Service {0} already registered.", typeof(T).Name));
}
return services;
}
private static Services ShouldContain<T>(this Services services)
{
if (!services.ContainsKey(typeof(T)))
{
throw new ServiceManagerException(String.Format("Service {0} is not registered.", typeof(T).Name));
}
return services;
}
}
Notice how I also use a fluent style within the public Register
and Get
methods.
Assuming we have a thread pool and publisher-subscriber objects, we can now register them as services:
Services services = new Services().Register(aThreadPool).Register(pubsub);
Unless the "programmer-user" inspects the types (granted, one has to put those "using..." alias at the top of the code file), this looks just like instantiated a class and calling methods of that class in a fluent notation.
One could take this concept of extension methods further, with something like:
private static Services ShouldNotContain<T>(this Services services)
{
return services.Assert(!services.ContainsKey(typeof(T)), String.Format("Service {0} already registered.", typeof(T).Name)));
}
or even worse:
services.Assert(!services.ContainsKey(typeof(T)), String.Format("Service {0} already registered.", typeof(T).Name)))[typeof(T)] = service;
but I find this to be going in the wrong direction -- the programmer has to read through a bunch of parameters and nested parens to figure out what's going on. And by the time they've unraveled the parameters and paren nesting, they've forgotten what the outer context is trying to accomplish, namely the mapping of a type to an implementation.
This actually leads to some good guidance on how extension methods should be used:
1. Write lower level methods that encapsulate the semantics of a behavior.
These two methods are good:
private static Services ShouldNotContain<T>(this Services services)
private static Services ShouldContain<T>(this Services services)
because they can be used in a semantically readable manner:
services.ShouldNotContain<T>()[typeof(T)] = service;
return (T)services.ShouldContain<T>()[typeof(T)];
But perhaps you disagree!
2. Avoid complicated nesting of parens.
This is readable:
Services services = new Services().Register(aThreadPool).Register(pubsub);
This becomes less readable:
Services services = new Services().Register(new ThreadPool()).Register(new SemanticPubSub());
So anywhere you end up with stringing (hah!) closing parens together, extract the inner operations until you get something clean.
Summary: In contrast to C#'s using
aliases, type definitions are not aliases, they are concrete types. New type definitions can be created from existing types. Type definitions can also be used to specify a function's parameters and return value. The forward pipe operator |>
is similar to the fluent "dot" notation in C#, but the value on the left of the |>
operator "populates" the last parameter in the function's parameter list. When functions are written that return something, the last function must be piped to the ignore function, which is slightly
awkward. F# type dependencies are based on the order of the files in the project, so a type must be defined before you use it.
I'm not an functional programming purist. The world is stateful and I have no problem using mutable C# classes in F# code -- dealing with things like tail recursion and monads to create purist immutable FP code might have a geek coolness to it but results in ridiculously hard to read code, at least for the beginner FP coder. Certainly every FP programmer should at least know about tail recursion, and probably monads and computational expressions as well. For the purposes of this article though, I wanted to maintain as much one-for-one comparison between the C# code and the F# code, thus the F# code leverages mutable C# classes. The other disclaimer is that I am not an FP expert by any stretch of the imagination, so if you are, and there are better ways of doing something, please share your knowledge with us all in the comments section of this article.
I had actually started with the thread pool service, which I'll discuss later, but regardless, at this point I started to realize that my coding looked a lot like functional programming:
- C# dot-notation vs. F# forward pipe operator
|>
- Extension methods vs. function definitions that can be curried or applied partially
- No classes, just types (granted, as aliases)
- Use cases in the thread pool and publisher-subscriber that utilize anonymous methods (functions)
So I decided to look at what an implementation in F# looks like.
First, the module (like a namespace, I'm not going into the details here) and the .NET references:
module ServiceManagerModule
open System
open System.Collections.Concurrent
Second, the type definitions:
type ServiceManagerException = Exception
type Service = Object
type Services = ConcurrentDictionary<Type, Service>
Wow, that's a lot cleaner because we're not creating aliases, we're creating actual types! Notice that the types don't need a fully qualified "namespace-dot-class" notation and that types can reference previously declared types.
We can also create types that "type" a couple functions:
type RegisterService = Service -> Services -> Services
type GetService<'T> = Services -> 'T
The first type reads "a function that takes a Service
and a Services
type and returns a Services
type".
The second type reads "a function with the generic parameter 'T
that takes a Services
type and returns a type of type 'T
.
Declaring a function type semantically rather than stringing a bunch of (var : type)
expressions together in the function itself can be very handy.
Next, the implementation:
let ShouldContain<'T> (services : Services) =
if not (services.ContainsKey(typeof<'T>)) then
raise (ServiceManagerException("Service is not registered."))
services
let ShouldNotContain<'T> (services : Services) =
if (services.ContainsKey(typeof<'T>)) then
raise (ServiceManagerException("Service already registered."))
services
let RegisterService:RegisterService = fun service services ->
(services |> ShouldNotContain).[service.GetType()] <- service
services
let GetService<'T> : GetService<'T> = fun services ->
(services |> ShouldNotContain).[typeof<'T>] :?> 'T
Notice the slight syntax differences when we specify the function type rather than inferring it from the function parameters and return value. Also notice the bizarre :?>
operator, which is the "downcast" operator, the equivalent of what we did in C# with the (T)
cast:
return (T)services.ShouldContain<T>()[typeof(T)];
Another advantage with F# is that the types that I declare don't have to be re-declared in every .fs file because F# code is compiled in the order of the project's .fs files -- as long as a .fs file declares the type before it is used lower down in the project's file list, you're fine. This is "dependency ordering", and can be awkward to work with if you have recursive type definitions. A good discussion on resolving this is here and there's also the and
keyword when working with forward type references within a single file, see an example here.
Lastly, notice that, as with C#, each function (exception for the GetService function) returns the Services type, allowing for the forward piping operator to continue evaluation additional expressions. This brings up something important -- the forward pipe operator always provides the last parameter (or parameters) in a function. So, to achieve the "fluent" style we were using in C#, the type we want to "forward on to the next function" needs to be the last parameter. Here's a simple example that illustrates this in F# interactive:
> let f a b = printfn "%d %d" a b;;
f 1 2;;
1 |> f 2;;
val f : a:int -> b:int -> unit
> 1 2
val it : unit = ()
> 2 1
val it : unit = ()
In the second form 1 |> f 2;;
notice that "1" is printed second. When writing functions with the intent to use forward piping (in general and as a fluent notation) this is an important design consideration.
Using this notation, we can write in F# what looks very similar in C# (except no parens):
let services = new Services() |> RegisterService threadPool |> RegisterService pubsub
But there's a caveat. let's say we want to write it this way:
let services = new Services()
services |> RegisterService threadPool |> RegisterService pubsub |> ignore
Notice the |> ignore
at the end. Because the function RegisterService
returns a type, F# (and this is true I believe with any FP language) expects you to do something with that type. Because we're not doing anything with that type, we have to pass it to the ignore function, which returns a "unit" -- meaning nothing. You can see this in the definition of the function "f" above:
val f : a:int -> b:int -> unit
Here, the function returns a "unit" because the printfn
function is defined to return a "unit."
As a result, a "fluent" notation in F# can result in littering your F# code with |> ignore
expressions. This can be avoided by having your F# functions return "unit", but then you lose the "fluency" of the syntax.
Summary: In C#, creating more complex aliases get messy real fast -- this is an experiment, not a recommendation for coding practices! In F#, we don't need an Action or Func class for passing functions because F# inherently supports type definitions that declare a function's parameters and return value -- in other words, everything in functional programming is actually a function. Tuples are a class in C# but native to functional programming, though C# 6.0 makes using tuples very similar to F#. While C# allows function parameters to be null, in F#, you have to pass in an actual function, even if the function does nothing.
The previous section covers everything, but I found it interesting to continue this approach in C# and F# to explore any further nuances, particularly the use of tuples. I'll illustrate the C# and F# code more side-by-side from here on. If you're wondering why I'm implementing my own thread pool instead of using .NET's ThreadPool
, the answer is related to performance, as discussed in this 14 year old article - in my latest testing, the ThreadPool
still behaves this way.
The C# version uses these aliases:
using Work = System.Action;
using ThreadExceptionHandler = System.Action<System.Exception>;
using ThreadGate = System.Threading.Semaphore;
using ThreadQueue = System.Collections.Concurrent.ConcurrentQueue<System.Action>;
using AThread = System.Tuple<System.Threading.Semaphore, System.Collections.Concurrent.ConcurrentQueue<System.Action>, System.Action<System.Exception>>;
using ThreadPool = System.Collections.Concurrent.ConcurrentBag<System.Tuple<System.Threading.Semaphore, System.Collections.Concurrent.ConcurrentQueue<System.Action>, System.Action<System.Exception>>>;
using ThreadAction = System.Tuple<System.Tuple<System.Threading.Semaphore, System.Collections.Concurrent.ConcurrentQueue<System.Action>, System.Action<System.Exception>>, System.Action>;
Notice how messy this gets! The actual thread pool object is aliased as ThreadPool
, the rest are for semantic convenience. To break that down for you:
using ThreadPool =
System.Collections.Concurrent.ConcurrentBag<
System.Tuple<
System.Threading.Semaphore,
System.Collections.Concurrent.ConcurrentQueue<
System.Action>,
System.Action<System.Exception>>>;
A thread pool is a collection of semaphore - queue - exception handler triplets, where each queue is an action. The idea is that a single semaphore manages each queue, where a separate thread is pulling data from the queue, and you can provide a custom exception handler for each thread that manages a queue (why I did this is not important and is actually sort of silly.)
Conversely, notice the type declarations in F#:
type Work = unit -> unit
type ThreadExceptionHandler = Exception -> unit
type ThreadGate = Semaphore
type ThreadQueue = ConcurrentQueue<Work>
type AThread = ThreadGate * ThreadQueue * ThreadExceptionHandler
type ThreadPool = ConcurrentBag<AThread>
type ThreadWork = AThread * Work
type GetWork = AThread -> ThreadWork
type AddWorkToQueue = Work -> AThread -> AThread
type AddWorkToPool = Work -> ThreadPool -> ThreadPool
There are a few extra here, including function type definitions, that improve the readability of the F# code. Notice we're not using C#'s Action
class, we're instead defining an item of work as function that takes no parameters and returns nothing:
type Work = unit -> unit
This is functional programming after all!
The C# implementation, as extension methods, looks like this:
public static class ThreadPoolExtensions
{
public static ThreadPool AddWork(this ThreadPool pool, Work work)
{
pool.MinBy(q => q.Item2.Count).AddWork(work).Item1.Release();
return pool;
}
public static ThreadPool Start(this ThreadPool pool)
{
pool.ForEach(aThread => aThread.Start());
return pool;
}
public static AThread Start(this AThread aThread)
{
new Thread(new ThreadStart(() => aThread.Forever(t => t.WaitForWork().GetWork().DoWork()))).IsBackground().StartThread();
return aThread;
}
private static Thread IsBackground(this Thread thread)
{
thread.IsBackground = true;
return thread;
}
private static Thread StartThread(this Thread thread)
{
thread.Start();
return thread;
}
private static AThread AddWork(this AThread aThread, Work work)
{
(var _, var queue, var _) = aThread;
queue.Enqueue(work);
return aThread;
}
private static AThread WaitForWork(this AThread aThread)
{
(var gate, var _, var _) = aThread;
gate.WaitOne();
return aThread;
}
private static ThreadAction GetWork(this AThread aThread)
{
Work action = null;
(var _, var queue, var _) = aThread;
queue.TryDequeue(out action);
return new ThreadAction(aThread, action);
}
private static ThreadAction DoWork(this ThreadAction threadAction)
{
((var _, var _, var exceptionHandler), var action) = threadAction;
exceptionHandler.Try(() => action());
return threadAction;
}
private static ThreadExceptionHandler Try(this ThreadExceptionHandler handler, Work action)
{
try
{
action?.Invoke();
}
catch (Exception ex)
{
handler?.Invoke(ex);
}
return handler;
}
}
There's a few things to note here.
public static void Forever<T>(this T src, Action<T> action)
{
while (true) action(src);
}
In F#, this is implemented as:
let rec Forever fnc =
fnc()
Forever fnc
Notice the rec
keyword -- this tells the compiler that the function will be called recursively (hence "rec") but because the function is declared recursively, it is implemented iteratively otherwise the program would eventually run out of stack space. You cannot actually write this function without the rec
keyword -- you get a compiler error that Forever
is not defined! Whew.
Tuples in C# 6 are much easier to work with. Note some examples of this syntax:
(var gate, var _, var _) = aThread;
gate.WaitOne();
Here we just care about the semaphore, the queue and exception handler can be ignored.
Work action = null;
(var _, var queue, var _) = aThread;
queue.TryDequeue(out action);
Here we only care about the queue, the semaphore and exception handler are ignored.
return new ThreadAction(aThread, action);
Here we are creating a tuple. Neat, eh?
Here:
pool.MinBy(q => q.Item2.Count).AddWork(work).Item1.Release();
We are finding a queue with the least amount of work queued on it and adding the work to that queue. As an aside, this is dubious at best because the determination of what thread to queue the work onto is based on the queue size, not whether the thread is already busy doing work! Ignore that, it's not the point of this article.
The F# implementation is similar -- notice the syntax similarity when working with tuples, which are more "native" to functional programming:
let AddWorkToThread:AddWorkToQueue = fun work athread ->
let gate, queue, _ = athread
queue.Enqueue(work)
gate.Release() |> ignore
athread
let AddWorkToPool:AddWorkToPool = fun work pool ->
pool.MinBy(fun (_, q, _) -> q.Count) |> AddWorkToThread work |> ignore
pool
let rec Forever fnc =
fnc()
Forever fnc
let WaitForWork(athread : AThread) =
let gate, _, _ = athread
gate.WaitOne() |> ignore
athread
let GetWork:GetWork= fun athread ->
let _, queue, _ = athread
let _, work = queue.TryDequeue()
(athread, work)
let DoWork threadWork =
try
(snd threadWork)()
with
| ex ->
let (_, _, handler) = fst threadWork
handler(ex)
threadWork
let StartThread athread : AThread =
let thread = new Thread(new ThreadStart(fun() -> Forever <| fun() -> athread |> WaitForWork |> GetWork |> DoWork |> ignore))
thread.IsBackground <- true
thread.Start()
athread
let StartThreadPool pool : ThreadPool =
for athread in pool do athread |> StartThread |> ignore
pool
I also was lazy and didn't implement the IsBackground()
and StartThread()
extension methods equivalent in F#.
Also notice in F# the "reverse pipe" operator <|
here:
fun() -> Forever <| fun() -> athread |> WaitForWork |> GetWork |> DoWork |> ignore
F#'s expressiveness has its advantages. We could also have written without the reverse pipe operator as:
let thread = new Thread(new ThreadStart(fun () -> Forever (fun() -> athread |> WaitForWork |> GetWork |> DoWork |> ignore)))
but that would require putting parens around the function that we want performed forever.
To state the obvious, C# has the concept of null
, so in the case that the user passes in a null
for the exception handler, we test that using the null conditional operator ?.
:
action?.Invoke();
Conversely, while F# has a concept of null
for compatibility with C# with regards to parameter passing and matching against return values, natively, the closest equivalence to C#'s null is the None
option value. You can't pass None
in as a parameter when a function is expected. In the code above, if you don't want to supply an exception handler, you still have to provide a "do nothing" function:
fun (_) -> ()
The _ is necessary because the exception handler expects a parameter. But this is exactly the definition of ignore
, so we can instead write:
let threadPool = new ThreadPool(Seq.map(fun _ -> (new ThreadGate(0, Int32.MaxValue), new ThreadQueue(), ignore)) {1..20})
Lincoln Atkinson has a great write-up on the topic of handling nulls in F# here.
Here's how we can use this in C# in a minimal way (arguably), without for loops, etc:
ThreadPool aThreadPool = new ThreadPool(Enumerable.Range(0, 20).Select((_) =>
new AThread(
new ThreadGate(0, int.MaxValue),
new ThreadQueue(),
ConsoleThreadExceptionHandler)));
DateTime startTime = DateTime.Now;
Enumerable.Range(0, 10).ForEach(n => aThreadPool.AddWork(() =>
{
Thread.Sleep(n * 1000);
Console.WriteLine("{0} thread ID:{1} when:{2}ms",
n,
Thread.CurrentThread.ManagedThreadId,
(int)(DateTime.Now - startTime).TotalMilliseconds);
int q = 10 / n;
}));
aThreadPool.Start();
This creates 20 threads and 10 worker items, the first of which throws an exception, and then starts the threads. The reason the threads are deferred is to force each work item to run on its own thread. Here's the output:
let exceptionHandler (ex : Exception) = printfn "%s" ex.Message
let threadPool = new ThreadPool(Seq.map(fun _ -> (new ThreadGate(0, Int32.MaxValue), new ThreadQueue(), exceptionHandler)) {1..20})
let startTime = DateTime.Now
for i in {0..9} do
threadPool |> AddWorkToPool (fun() ->
let q = 10 / i
Thread.Sleep(i * 1000)
printfn "%d Thread ID:%d when:%ims" i Thread.CurrentThread.ManagedThreadId (int ((DateTime.Now - startTime).TotalMilliseconds)))
|> StartThreadPool |> ignore
Notice here I use a for
loop - I could have done Seq.map
again, but F#'s for - in
syntax is so much more expressive that C#'s that it only obscures to code in my opinion. The advantage of using Seq.map
in the instantiation of the ThreadPool
is that this creates a collection (though we ignore the sequence number). The underlying type, ConcurrentBag<AThread>
, accepts an IEnumerable
as a parameter. (I must say there is a really nice interoperability between C# and F#.) This is additional code obfuscation if we iterate a simple Seq
in F# - the {0..9}
is the sequence!
And the output:
There is a slight difference because of how I write the code. I cannot write:
Thread.Sleep(i * 1000)
printfn "%d Thread ID:%d when:%ims" i Thread.CurrentThread.ManagedThreadId (int ((DateTime.Now - startTime).TotalMilliseconds))
let q = 10 / i
because a "let" cannot be the final code element in a block - a function must evaluate to something, at a minimum a "unit", and a let statement as the last code element in a function is an assignment function, not an evaluation. To fix that, I would have to write it to return a "unit" like this:
Thread.Sleep(i * 1000)
printfn "%d Thread ID:%d when:%ims" i Thread.CurrentThread.ManagedThreadId (int ((DateTime.Now - startTime).TotalMilliseconds))
let q = 10 / i
()
Then I get the same output as in the C# example:
Ah, the nuances of functional programming! Also, I have no idea why the thread ID's are so different between C# and F#.
Summary: F# uses a nominal ("by name") as opposed to structural inference engine, so in F#, giving types semantically meaningful names is very important so that the type inference engine can infer the correct type.
The last piece! The idea here is to register subscribers will be triggered when a particular type is published. The caller can determine whether the subscribers of the type should process the type immediately or whether processing can be performed asynchronously using the thread pool created earlier.
These are the aliases defined by the pub-sub:
using PubSubExceptionHandler = System.Action<System.Exception>;
using PubSubTypeReceiverMap = System.Collections.Concurrent.ConcurrentDictionary<System.Type, System.Collections.Concurrent.ConcurrentBag<object>>;
using PubSubReceivers = System.Collections.Concurrent.ConcurrentBag<object>;
using SemanticPubSub = System.Tuple<System.Collections.Concurrent.ConcurrentDictionary<System.Type, System.Collections.Concurrent.ConcurrentBag<object>>, System.Action<System.Exception>>;
using ThreadPool = System.Collections.Concurrent.ConcurrentBag<System.Tuple<System.Threading.Semaphore, System.Collections.Concurrent.ConcurrentQueue<System.Action>, System.Action<System.Exception>>>;
Notice that the ThreadPool
alias has to be included, as we're using the thread pool for the async publish process:
Again, this gets nasty, so here's the breakdown of the SemanticPubSub
type:
SemanticPubSub = System.Tuple<
System.Collections.Concurrent.ConcurrentDictionary<
System.Type,
System.Collections.Concurrent.ConcurrentBag<object>>,
System.Action<System.Exception>>;
The SemanticPubSub
is a map - exception handler pair, where the map associates a type with a collection of receivers that act on an instance of the type, though that last part isn't obvious because it's a collection of objects. As with the service manager, we would ideally use an interface type so we could replace object
with Action<ISubscriberData>
or something like that, but the cast, when we call the subscribers, handles this for us. We may also not have the ability to add ISubscriberData
to the type that's being passed to the subscribers, particularly if we don't have the code for the data.
In F#, this is a lot more readable:
open ThreadPoolModule
type PubSubExceptionHandler = Exception -> unit
type Subscribers = ConcurrentBag<Object>
type Subscription<'T> = 'T -> unit
type Subscriptions = ConcurrentDictionary<Type, Subscribers>
type SemanticPubSub = Subscriptions * PubSubExceptionHandler
type Subscribe<'T> = Subscription<'T> -> SemanticPubSub -> SemanticPubSub
type Publish<'T> = 'T -> SemanticPubSub -> SemanticPubSub
type AsyncPublish<'T> = ThreadPool -> 'T -> SemanticPubSub -> SemanticPubSub
type CreateMissingBag = SemanticPubSub -> SemanticPubSub
Notice the open ThreadPoolModule
, which pulls in the types defined in that module. Here again we use Object
for the list of subscribers. I've been exploring the concept of statically resolving type parameters in F# with the ^T
notation, as well as Tomas Petricek's article on dynamic lookup, but my F#-fu is not there yet.
These types are function type declarations:
type PubSubExceptionHandler = Exception -> unit
type Subscription<'T> = 'T -> unit
type Subscribe<'T> = Subscription<'T> -> SemanticPubSub -> SemanticPubSub
type Publish<'T> = 'T -> SemanticPubSub -> SemanticPubSub
type AsyncPublish<'T> = ThreadPool -> 'T -> SemanticPubSub -> SemanticPubSub
This takes an Exception and returns nothing:
type PubSubExceptionHandler = Exception -> unit
This takes a generic parameter of type T and returns nothing:
type Subscription<'T> = 'T -> unit
This takes a Subscription and the pubsub and returns the pubsub:
type Subscribe<'T> = Subscription<'T> -> SemanticPubSub -> SemanticPubSub
This takes a generic parameter and the pubsub and returns a pubsub:
type Publish<'T> = 'T -> SemanticPubSub -> SemanticPubSub
This takes a generic type, a thread pool, and the pubsub and returns a pubsub:
type AsyncPublish<'T> = ThreadPool -> 'T -> SemanticPubSub -> SemanticPubSub
So, reading this, from the bottom up:
type PubSubExceptionHandler = Exception -> unit
type Subscribers = ConcurrentBag<Object>
type Subscriptions = ConcurrentDictionary<Type, Subscribers>
type SemanticPubSub = Subscriptions * PubSubExceptionHandler
A SemanticPubSub
is a tuple consisting of subscriptions and an exception handler, where each subscription is a map between a Type
and collection of subscribers implemented as an Object
. Why an object? Because the subscription:
type Subscription<'T> = 'T -> unit
defines a generic type as a parameter, and this type varies and is not known to the pub-sub. Yes, as with C#, this could be implemented by enforcing that any subscription implements an interface, but particularly in functional programming, where the type can be anything, including another function, we don't want to enforce the requirement that the generic parameter is "class-like."
Again, extension methods are used for the implementation. The three methods that are exposed are:
- Subscribe
- Publish
- AsyncPublish
Note that AsyncPublish
is not awaitable (it's not supposed to be awaitable), otherwise I would have called the method PublishAsync
for naming convention consistency.
public static class SemanticPubSubExtensionMethods
{
public static SemanticPubSub Subscribe<T>(this SemanticPubSub pubsub, Action<T> receiver)
{
pubsub.CreateMissingBag<T>().Item1[typeof(T)].Add(receiver);
return pubsub;
}
public static SemanticPubSub Publish<T>(this SemanticPubSub pubsub, T data)
{
if (pubsub.Item1.ContainsKey(typeof(T)))
{
pubsub.Item1[typeof(T)].ForEach(r => pubsub.Item2.Try((Action<T>)r, data));
}
return pubsub;
}
public static SemanticPubSub AsyncPublish<T>(this SemanticPubSub pubsub, ThreadPool threadPool, T data)
{
if (pubsub.Item1.ContainsKey(typeof(T)))
{
pubsub.Item1[typeof(T)].ForEach(r => threadPool.AddWork(() => ((Action<T>)r)(data)));
}
return pubsub;
}
private static SemanticPubSub CreateMissingBag<T>(this SemanticPubSub pubsub)
{
Type t = typeof(T);
if (!pubsub.Item1.ContainsKey(t))
{
pubsub.Item1[t] = new PubSubReceivers();
}
return pubsub;
}
private static PubSubExceptionHandler Try<T>(this PubSubExceptionHandler handler, Action<T> action, T data)
{
try
{
action?.Invoke(data);
}
catch (Exception ex)
{
handler?.Invoke(ex);
}
return handler;
}
}
Note the downcast ((Action<T>)r)(data))
which is perfectly safe because we are only calling methods that subscribed with type T
.
The F# implementation is similar, again note the downcast operator :?>
let CreateMissingBag<'T> : CreateMissingBag = fun pubsub ->
let t = typeof<'T>
let (dict, _) = pubsub
if not (dict.ContainsKey(t)) then
dict.[t] <- new Subscribers();
pubsub
let Subscribe<'T> : Subscribe<'T> = fun fnc pubsub ->
let t = typeof<'T>
let (dict, _) = pubsub |> CreateMissingBag<'T>
dict.[t].Add(fnc)
pubsub
let TryPublish fnc pubsub =
let (_, handler) = pubsub
try
fnc()
with
| ex ->
handler(ex)
let Publish<'T> : Publish<'T> = fun data pubsub ->
let t = typeof<'T>
let (dict, handler) = pubsub
if (dict.ContainsKey(t)) then
for subscriber in dict.[t] do
pubsub |> TryPublish (fun () -> data |> (subscriber :?> Subscription<'T>))
pubsub
let AsyncPublish<'T> : AsyncPublish<'T> = fun threadPool data pubsub ->
let t = typeof<'T>
let (dict, _) = pubsub
if (dict.ContainsKey(t)) then
for subscriber in dict.[t] do
threadPool |> AddWorkToPool (fun () -> data |> (subscriber :?> Subscription<'T>)) |> ignore
pubsub
Here's a C# usage example -- note the fluent notation and anonymous method:
public class Counter
{
public int N { get; protected set; }
public Counter(int n) { N = n; }
}
public class SquareMe
{
public int N { get; protected set; }
public SquareMe(int n) { N = n; }
}
pubsub.Subscribe<Counter>(r =>
{
int n = r.N;
Thread.Sleep(n * 1000);
Console.WriteLine("{0} thread ID:{1} when:{2}ms",
n,
Thread.CurrentThread.ManagedThreadId,
(int)(DateTime.Now - startTime).TotalMilliseconds);
int q = 10 / n;
}).Subscribe<SquareMe>(r =>
{
Console.WriteLine("Second subscriber : {0}^2 = {1}", r.N, r.N * r.N);
});
The above creates a couple subscribers, and this is how the types are published -- the Counter
instances are published asynchronously, the SquareMe
instances is published synchronously:
Enumerable.Range(0, 10).ForEach(n => pubsub.AsyncPublish(aThreadPool, new Counter(n)).Publish(new SquareMe(n)));
The result is:
The same example, but in F#:
type Counter = {N : int}
type SquareMe = {N : int}
pubsub |> Subscribe<Counter> (fun r ->
let n = r.N
let q = 10 / n
Thread.Sleep(n * 1000)
printfn "%d Thread ID:%d when:%ims"
n
Thread.CurrentThread.ManagedThreadId
(int ((DateTime.Now - startTime).TotalMilliseconds)))
|> Subscribe<SquareMe> (fun r -> printfn "%d^2 = %d" r.N (r.N * r.N)) |> ignore
Note the brevity of creating a "record" type, otherwise everything else looks pretty much the same. Publishing the two instances is also very similar:
for i in {0..9} do
pubsub |> AsyncPublish tp {Counter.N = i} |> Publish {SquareMe.N = i} |> ignore
Again, a bit more concise (terser, perhaps) than the C# code. Do note however that the we have to explicitly state which record to create:
{Counter.N = i}
{SquareMe.N = i}
because both records are nominally (as in, by "name" - nominclature) the same, having a single member N
. If we named them this way:
type Counter = {Ctr : int}
type SquareMe = {Sqr : int}
Then publishing these record instances would be even terser (fixing up the subscriber methods as well):
for i in {0..9} do
ps |> AsyncPublish tp {Ctr = i} |> Publish {Sqr = i} |> ignore
This points out the usefulness of giving types semantically meaningful names so that the type inference engine can infer the type and also the dangers of having records that have the same names - F#'s inference engine will use the last record, so if we do this:
type Counter = {N : int}
type SquareMe = {N : int}
without specifying the record type:
ps |> AsyncPublish tp {N = i} |> Publish {N = i} |> ignore
we get this:
Note the odd output due to the fact that one set of the computations are running on separate threads. In the code above, it's rather obvious that something is wrong, but this is less obvious with more complex code. F# uses nominal type inference rather than structural type inference, so this can result in very strange compiler error messages if you think you're using type A when instead your using type B.
Summary: Here we see that all the using aliases in C# have to be pulled in to this file. With F#, we don't have to declare the types over again.
Lastly, we want both the thread pool and the pub-sub to be services, so we'll tie in the service manager here.
Here's what the final example in C# looks like, demonstrating that we're getting the pubsub and thread pool services from the service manager. Notice we have all those ugly using
aliases:
using System;
using System.Linq;
using System.Threading;
using Services = System.Collections.Concurrent.ConcurrentDictionary<System.Type, object>;
using PubSubTypeReceiverMap = System.Collections.Concurrent.ConcurrentDictionary<System.Type, System.Collections.Concurrent.ConcurrentBag<object>>;
using SemanticPubSub = System.Tuple<System.Collections.Concurrent.ConcurrentDictionary<System.Type, System.Collections.Concurrent.ConcurrentBag<object>>, System.Action<System.Exception>>;
using ThreadGate = System.Threading.Semaphore;
using ThreadQueue = System.Collections.Concurrent.ConcurrentQueue<System.Action>;
using AThread = System.Tuple<System.Threading.Semaphore, System.Collections.Concurrent.ConcurrentQueue<System.Action>, System.Action<System.Exception>>;
using ThreadPool = System.Collections.Concurrent.ConcurrentBag<System.Tuple<System.Threading.Semaphore, System.Collections.Concurrent.ConcurrentQueue<System.Action>, System.Action<System.Exception>>>;
namespace MinApp
{
public class Counter
{
public int N { get; protected set; }
public Counter(int n) { N = n; }
}
public class SquareMe
{
public int N { get; protected set; }
public SquareMe(int n) { N = n; }
}
class Program
{
static void Main(string[] args)
{
ThreadPool aThreadPool = new ThreadPool(Enumerable.Range(0, 20).Select((_) =>
new AThread(
new ThreadGate(0, int.MaxValue),
new ThreadQueue(),
ConsoleThreadExceptionHandler)));
SemanticPubSub pubsub = new SemanticPubSub(new PubSubTypeReceiverMap(), ConsolePubSubExceptionHandler);
Services services = new Services().Register(aThreadPool).Register(pubsub);
var pool = services.Get<ThreadPool>();
DateTime startTime = DateTime.Now;
pubsub.Subscribe<Counter>(r =>
{
int n = r.N;
Thread.Sleep(n * 1000);
Console.WriteLine("{0} thread ID:{1} when:{2}ms", n, Thread.CurrentThread.ManagedThreadId, (int)(DateTime.Now - startTime).TotalMilliseconds);
int q = 10 / n;
}).Subscribe<SquareMe>(r =>
{
Console.WriteLine("Second subscriber : {0}^2 = {1}", r.N, r.N * r.N);
});
Enumerable.Range(0, 10).ForEach(n => pubsub.AsyncPublish(pool, new Counter(n)).Publish(new SquareMe(n)));
pool.Start();
Console.ReadLine();
}
static void ConsoleThreadExceptionHandler(Exception ex)
{
Console.WriteLine(ex.Message);
}
static void ConsolePubSubExceptionHandler(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
By contrast, here's the F# example. Notice we don't have to redefine the types:
open System
open System.Collections.Concurrent
open System.Threading
open ThreadPoolModule
open SemanticPubSubModule
open ServiceManagerModule
type Counter = {N : int}
type SquareMe = {N : int}
[<EntryPoint>]
let main argv =
let exceptionHandler (ex : Exception) = printfn "%s" ex.Message
let threadPool = new ThreadPool(Seq.map(fun _ -> (new ThreadGate(0, Int32.MaxValue), new ThreadQueue(), exceptionHandler)) {1..20})
let pubsub = (new Subscriptions(), exceptionHandler)
let services = new Services()
services |> RegisterService threadPool |> RegisterService pubsub |> ignore
let startTime = DateTime.Now
let tp = services |> GetService<ThreadPool>
let ps = services |> GetService<SemanticPubSub>
ps |> Subscribe<Counter> (fun r ->
let n = r.N
let q = 10 / n
Thread.Sleep(n * 1000)
printfn "%d Thread ID:%d when:%ims" n Thread.CurrentThread.ManagedThreadId (int ((DateTime.Now - startTime).TotalMilliseconds)))
|> Subscribe<SquareMe> (fun r -> printfn "%d^2 = %d" r.N (r.N * r.N)) |> ignore
for i in {0..9} do
ps |> AsyncPublish tp {Counter.N = i} |> Publish {SquareMe.N = i} |> ignore
tp |> StartThreadPool |> ignore
Console.ReadLine() |> ignore
0
Extending Behaviors - The Dangers in Object Oriented Programming and the Argument for Functional Programming
Summary: In C#, changing the members of class doesn't affect the class type. Not so with F# (at least with vanilla records) -- changing the structure of a record changes the record's type. Changing the members of a C# class can, among other things, lead to incorrect initialization and usage. Inheritance, particularly in conjunction with mutable fields, can result in behaviors with implicit understanding like "this will never happen" to suddenly break. Extension methods and overloading creates semantic ambiguity. Overloading is actually not supported in F# - functions must have semantically different names, not just different types or parameters lists.
As an exercise in minimizing the creating of classes in C# code, while it is doable with using
aliases and extension methods, it is a very non-standard approach. The using
aliases are ugly, unwieldy, and totally break the rules of OOP, particularly encapsulation and inheritance, making anything you write in this form brittle because it will completely break your code everywhere if you make a change to the organization of a tuple or dictionary. Also, aliases allow direct manipulation of the dictionary, collection or tuple, which is another problem, because they are mutable -- a problem I carried along in the F# examples.
Typically, the maintainability (by this I mean that at some point you're going to want to extend the behavior of existing code) of OOP code relies on one or more of the following options:
- Change the class -- if you have the luxury of having the source code, you can change the class (add/remove methods/fields/properties/delegates) and at worse you have to recompile all the code depending on the concrete implementation.
- Inheritance -- if you can't change the code but class isn't
sealed
, you might at least be able to create a new type that inherits the behaviors of the old type and lets you extend the behavior in your inherited type. - Encapsulation -- if you can't inherit the type (as in, it's
sealed
) you can implement a wrapper for the class and pass through the behaviors you want to keep the same, change/add/remove other behaviors. - Extension methods -- another way of adding behaviors, but not member variables, to a class, whether it's sealed or not.
- Wrapper methods - what we used to do before extension methods (no further discussion on this option.)
So let's take a considerable detour and look at these OOP techniques with C# and what happens when we try them with F#. Along the way I'll come up with some of the reasons why OOP can be dangerous, why functional programming avoids those dangers, and how even in functional programming to enter into the danger zone.
With regards to C#, changing the container does not change the container's type. For example:
public class Foo
{
public int A { get; set; }
}
public class UsesFoo
{
public void CreateFoo()
{
var foo = new Foo() { A = 5 };
}
}
We now change Foo:
public class Foo
{
public int A { get; set; }
public int B { get; set; }
}
The method CreateFoo
still works, though something else will probably break because B
isn't initialized.
Now consider this in F#, using a record (a record is an aggregate of named values and are reference types by default, like classes as opposed to structs):
type Foo = {A : int}
let bar = {A=5}
Now let's change Foo
, adding B
:
Whoops! We have changed the type! Now granted, we could use classes in F#, but the point (academic, perhaps) of this whole article is to avoid using classes!
This is sort of like doing this in C#:
using Foo = System.Int32;
...
public void FooType()
{
var foo = new Foo();
foo = 5;
}
and then, changing Foo
:
using Foo = System.Tuple<System.Int32, System.Int32>;
Ah! So here we get to what I consider to be a critical flaw in object oriented programming. When we modify a class on OOP, the type definition for that class doesn't change! This is a great convenience but also inherently dangerous because it results in the usage of the type with potentially unexpected results when members of the class are improperly initialized.
The contrived F# equivalent example would be more like this:
type Foo2 = int
let mutable bar2 = new Foo2()
bar2 <- 5
Then changing Foo2 to:
type Foo2 = int * int
The point being that F# (as long as you don't use classes) doesn't even allow us to modify the "aliasing" of a primitive type, if you were to do something as silly as the above contrived example.
Consider this example instead:
public class Foo
{
public int A { get; set; }
}
public class Foo2 : Foo
{
public int B { get; set; }
}
public class UsesFoo
{
public void CreateFoo()
{
var foo = new Foo() { A = 5 };
}
}
Here we have preserved the "good" behavior of Foo
in all places that use Foo
, and we have introduced a new type Foo2
for when we want the extended behavior.
This is a good practice on OOP, especially once your code is in production -- anything new that want to do that leverages Foo
should be derived from Foo so you don't inadvertently break some existing usage!
The F# equivalent, using records, would look like this:
type Foo = {A : int}
type Foo2 = {foo : Foo; B : int}
let bar = {A=5}
let bar2 = {foo=bar; B=5}
But this isn't inheritance, it's encapsulation! You can't inherit from a record type because record types are compiled to sealed classes!
So the actual C# equivalent would be:
public class Foo2
{
public Foo Foo { get; set; }
public int B { get; set; }
}
The advantage of inheritance in C# is that Foo2 acquires all the public/protected members of Foo
. However, if you make the encapsulated member Foo
protected
or private
, you have to explicitly expose the members that you still want to pass through to Foo
. There may be good reasons to do this - for example, throwing an exception on an inappropriate use of Foo2
.
You cannot do this in F#:
All record fields must be designated as private:
type Foo2 = private {foo : Foo; B : int}
let bar = {A=5}
let bar2 = {foo=bar; B=5}
let a = bar2.foo.A
let b = bar2.B
And as the code above illustrates, it gets weirder, if you want to read this.
Inheritance, in F#, is not the same as using the record type:
type Foo(a) =
member this.A = a
type Foo2(a, b) =
inherit Foo()
member this.B = b
This is actually the correct equivalent to the C# example, and as I discuss in the section "Inheritance and Mutable and Immutable" below.
If you can't change the class members and you can't inherit the class, and you don't want to encapsulate the class, you can still extend the behavior of a class with extension methods. Consider this contrived example:
public static class Extensions
{
public static int Add(this Foo foo, int val)
{
return foo.A + val;
}
}
public class Foo
{
public int A { get; set; }
public int Add()
{
return A + 1;
}
}
public class UsesFoo
{
public void CreateFoo()
{
var foo = new Foo() { A = 5 };
int six = foo.Add();
int seven = foo.Add(2);
}
}
Notice we have effectively overloaded the poorly named Add
method with an extension method of the same name. Granted, while this is a contrived example, extension methods (introduced in C# 2.0) are a great convenience as previously we had to write static helper classes to extend the behavior of sealed classes. And while object oriented programming has supported method overloading (aka, one of the features of polymorphism) from basically day 1, it can easily lead to semantic confusion if used inappropriately.
Conversely, in F#, overloading is not permitted (unless you go through some hoops, see below):
While you can coerce F# to use extension methods like this (thanks to TheBurningMonk for an example):
open System.Runtime.CompilerServices
type Foo = {A : int}
let Add (foo : Foo) = foo.A + 1
[<Extension>]
type FooExt =
[<Extension>]
static member Add(foo : Foo, n : int) = foo.A + n
let foo = {A = 5}
let six = foo.Addlet seven = foo.Add(2)
Do not do this! It breaks the purity of functional programming, which, among other things, enforces good semantic naming practices:
type Foo = {A : int}
let IncrementFoo (foo : Foo) = foo.A + 1
let AddToFoo (foo : Foo) (n : int) = foo.A + n
let foo = {A = 5}
let six = IncrementFoo foo
let seven = 2 |> AddToFoo foo
let eight = AddToFoo foo 3
Lastly, we come to what every imperative language supports: mutability.
public class Foo
{
public int A { get; set; }
public void Increment()
{
A = A + 1;
}
}
In F#, the =
operator is a comparison, not an assignment:
If you want to make an assignment, you have to use the <-
operator:
Oops. You have to make the type mutable
:
type Foo = {mutable A : int}
let IncrementFoo (foo : Foo) =
foo.A <- foo.A + 1
let foo = {A = 5}
let b = IncrementFoo foo
This demonstrates another problem with OOP. Let's say you have this (granted, contrived):
public class Foo
{
public int A { get; set; }
}
public class Foo2 : Foo
{
public void Increment()
{
A = A + 1;
}
}
class Program
{
static void Main(string[] args)
{
var myFoo = new Foo2() { A = 5 };
myFoo.Increment();
ShowYourFoo(myFoo);
}
static void ShowYourFoo(Foo foo)
{
Console.WriteLine("Hey, who change my A! " + foo.A);
}
}
Mutability combined with inheritance lets me change my inherited concept of an instance in ways the original implementation did not expect! But wait, the whole point of inheritance and mutability is to do exactly that, and yes, there are many advantages to this scenario. There are also many dangers:
class Program
{
static void Main(string[] args)
{
var myFoo = new Foo2() { A = -1 };
myFoo.Increment();
DivideByA(myFoo);
}
static void DivideByA(Foo foo)
{
int d10 = 10 / foo.A;
}
}
Come now, how many times have you discovered a bug (in production, no less) where an existing method in a base class, having been implemented with knowledge that certain things cannot happen, suddenly breaks because that very same method of the base class now breaks because of something a derived class changed?
Yes, you can coerce F# into the same problem, and there are many times when a mutable type is required -- I always argue that pure functional programming is unrealistic because we live in a mutable world. Realistically though, functional programming helps you avoid mutation when you don't really need it. It does this by requiring that you use the mutable keyword and (again, as long as you avoid inheritance) by constructing new types with semantic names for the functions that operate on those types.
With regards to inheritance in F#, this is reasonable because the types are immutable:
type Foo(a) =
member this.A = a
member this.Increment() =
this.A + 1
type Foo2(a, b) =
inherit Foo(a)
member this.B = b
let foo = new Foo2(1, 2)
let newA = foo.Increment()
This example:
type Foo(a) =
let mutable A = a
member this.Increment() =
A <- A + 1
type Foo2(a, b) =
inherit Foo(a)
member this.B = b
let foo = new Foo2(1, 2)
foo.Increment()
Gets you into the same problem as described above. The conclusion here (which one can argue until the cows come home) is that inheritance with mutable types is dangerous!
What to say? While you'd never code in C# like this, it's a lot more natural in F# and I would imagine with functional programming in general. In the process of writing this, I think I identified some concrete pitfalls of OOP that functional programming overcomes, though FP (at least with F#) can definitely be coerced to exhibit the same behaviors. I also believe I came up with some specific differences with regards to object oriented vs. functional programming -- not that one is better than the other, but at least specific talking points to the advantages and disadvantages of both. Hopefully you will concur!