F# to C#: expose as common types
The common case is that pure F# code is used in unpure C# code. F# is a great way to design strict rules and application core code, while C# is great at handling nifty and cumbersome interactions with the outside world. Let’s go over some practical examples. The common idea is that we should make sure that we don’t expose language-specific types but rather use types that both code languages are familiar with. ‘Speaking their language’ is one of the key ideas here.
F# uses camelCase for functions, while C# uses PascalCase for methods. When you want to expose a function, you can easily apply the
CompileNameAttribute to your function and give it the PascalCase alternative.
Lists, options, delegates and async operations
Async types work well in C#. Keep in mind that you need to expose an alternative. For
List types, this can be
Async, this could be
Task. If you want to take in a function, make sure to use a delegate so that C# can easily use
Func and Action. An interesting type to expose is
Option. As C# doesn’t have such an optional type, we can’t use an alternative. Usually, this is fixed by exposing a
Try... function with an output variable. This is a common practice in C# to avoid exception handling and is a good alternative for exposing F#
This example shows another good practice: using tupled methods stabilizes the exposed signature and clearly shows other code that it is for C# usages.
💡 Speaking of which, if you want to force your exposed F# code for only C# usages, you can add
CompilerMessage("Not designed for F#", 1001, IsHidden = true) to your code. The
IsHidden property will make sure that the code is compiled but will not show up when developing F# code. This is a very cool feature that helps to organize your code, as the F# API won’t get overwhelmed with interop code, while the C# API can still benefit from it.
Function composition as extensions
A final tip is to expose function composition as extension methods. F# is built for composition, but C# is not. What C# has is an extension method that allows you to ‘pipe’ results. Like using types that the C# environment understands, it is a good alternative to helping the C# developer interact with your code without overwhelming them.
The following example shows you a combination of all the practices in this section: naming, argument checking, interop hiding from F#, exposing known types and functions as extensions:
Notice that the
IEnumerable is a common type in C# with many LINQ extensions. Function composition could be treated the same way if you want to expose it in C#. Since the
seq are interchangeable, we don’t need any additional conversions here. The
Action will need to be called with the
Invoke method, to step away from the object and use the function instead. It is a clear indication that you step into the realm of functions instead of objects. Placing the extension in a namespace that will be easily discovered is also a way to help your C# developers.
C# to F#: forcing good practice
In certain cases, you want to use C# code in F#. An example is when you want to use F# as a ‘pipeline component’ between two C# code projects. Maybe F# is a better language to solve a specific problem and you would like to take a detour via F#.
To use C# code easier in an F# environment, there are some best practices to take into account. You’ll notice that most practices will make your C# code only better.
The constructor takes it all
Regular class properties are supported in F#, but setting properties is not very clean and easy to use code (with reason!). Changing a property value is seen as a ‘side effect’, and should be treated with caution. Immutability is by default present in F#, so it’s a good practice to write the classes that you use in F# as immutable classes. What this means is that you should limit the use of property settings and make sure that the class constructor takes all the necessary information from the start.
Don’t trust external classes
While C# is working on removing
null from being a ‘valid valid’, we still could have situations where
null values happen. When you take in a C# type in F# code, don’t assume that it will be safe(r) than any other F# type (records, discriminated unions). Check for
isNull and throw with
nullArg like general C# code. After that, you can be a little safer, but be still aware that you’re using an unsafe type. This is also the case with C#
record classes. They are still treated like regular classes in F# and are not transferable to F# records, so check for
Don’t overdo method overloading
As F# highly leans on implicit generic constructs, method overloading could be a problem. Ambiguous errors could occur if multiple overloads are ‘valid’ to be used but the type system doesn’t know what type to use. Use factory methods instead, otherwise F# will have to add a lot of argument names to make the distinction between method overloads clear.
Combining these two .NET languages is a great way to get best of both worlds. We should, however, be cautious of what we expose to the other language, as the way they treat data and actions are quite different. A good common practice is to keep it simple and use the basic syntax and expressions in your API. Both are .NET languages so they both support all the underlying types. The exchange should happen on that level. You’ll see that these languages are not battling against each other, but are making each other great.
💡 My own FPrimitive library is proof that a .NET package can support both languages without taking back on the exposed functionality. Please consider supporting both languages for your next library so we can work together on a more inclusive code space.
Thanks for reading.
Subscribe to our RSS feed