F# - C# Interop
source link: https://dev.to/amedeov/f-c-interop-2a42
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
What is Interop
Interoperability is the ability of two different systems to communicate with each other. In this case, it would be a C# and F# application communicating. Because they compile down to the same Intermediate Language we can reference F# projects in C# projects and vice versa.
Why is this useful
One of the benefits is that if you are planning to migrate your codebase from C# to F#, you can do it gradually by introducing some F# code without discarding any of the existing code base.
Sometimes we want to take advantage of the C# world, i.e. a lot of existing nuget packages are written in C# and we don't want to rewrite them in F#.
⚠️ Please note that all the observations listed on this page are valid for .NET 5 and below.
Example F# Application using C# library
In this example, we are going to create an F# application that uses a C# library.
The library simulates an IO operation with await Task.Delay(300, cancellationToken);
and reverses the words sent.
C# Library
namespace WordReverserLibraryCsharp
{
public static class WordReverser
{
public static async Task<string> WordReverserAsync(string sentence, CancellationToken cancellationToken)
{
await Task.Delay(300, cancellationToken);
var words = sentence.Split(' ');
Array.Reverse(words);
return $"{string.Join(" ", words)}";
}
}
}
Enter fullscreen mode
Exit fullscreen mode
F# Application
namespace WordReverserConsoleAppFsharp
open System.Threading
open WordReverserLibraryCsharp
module WordReverserModule =
let wordReverser (sentence: string) : Async<unit> =
async {
let cancellationTokenSource = new CancellationTokenSource(800);
let! reversedWords =
WordReverser.WordReverserAsync(sentence, cancellationTokenSource.Token)
|> Async.AwaitTask
printfn ($"{reversedWords}")
}
Async.Start(wordReverser("one two three"))
Async.RunSynchronously(Async.Sleep(1000))
Enter fullscreen mode
Exit fullscreen mode
Observations
To consume the C# library from F#, we have to convert the Task<T>
returned from the library to Async<T>
using Async.AwaitTask
which waits for the Task to complete and returns its result as Async<T>
. Read about Async.AwaitTask here
Just before the line Async.Start(wordReverser("one two three"))
we still haven't invoked the operation, only done the setup to call wordReverser
, to start the operation we can use Async.Start(wordReverser("one two three")
.
Example C# Application using F# library
In this example we are doing the opposite of the previous one: we create a C# application that uses an F# library.
F# Library
namespace WordReverserLibraryFsharp
open System.Threading
open System.Threading.Tasks
module WordReverserModule =
let wordReverser (sentence: string, cancellationToken: CancellationToken) : Task<string> =
let reverser =
async {
do! Async.Sleep(300)
return
sentence.Split [| ' ' |]
|> Array.rev
|> String.concat " "
}
Async.StartAsTask(reverser, TaskCreationOptions.None, cancellationToken)
Enter fullscreen mode
Exit fullscreen mode
C# Application
using WordReverserLibraryFsharp;
namespace WordReverserConsoleAppCsharp
{
internal static class Program
{
private static async Task Main()
{
var cancellationTokenSource = new CancellationTokenSource(400);
var reversedWords = await WordReverserModule.wordReverser("one two three", cancellationTokenSource.Token);
Console.WriteLine(reversedWords);
}
}
}
Enter fullscreen mode
Exit fullscreen mode
Observations
We can't use F# Async
in C#, but the F# library can return a Task
so that the C# application can consume it without any issues. We can achieve this by using Async.StartAsTask
in .NET 5 and below.
Read about Async.StartAsTask here
It's good practice for any Async
work in C#, to always pass a CancellationToken as an argument, while the F# code does not necessarily need one as the CancellationToken propagation is controlled by how the asynchronous work is kicked off and as a result, CancellationTokens may or may not be propagated. For example Async.Sleep
doesn't accept a CancellationToken as a parameter.
Example C# Application using F# library handling exceptions
In this example, we are going to use an F# library from a C# application, handling exceptions and passing a CancellationToken.
F# Library
namespace FsharpLibraryComplete
open System.Threading
open System.Threading.Tasks
module FsharpLibraryCompleteModule =
let wordReverser (sentence: string, raiseException: bool, cancellationToken: CancellationToken) : Task<string> =
let reverser =
async {
do! Async.Sleep(300)
if(raiseException) then
raise (System.Exception("wordReverser threw Exception"))
return
sentence.Split [| ' ' |]
|> Array.rev
|> String.concat " "
}
Async.StartAsTask(reverser, TaskCreationOptions.None, cancellationToken)
Enter fullscreen mode
Exit fullscreen mode
C# Application
using FsharpLibraryComplete;
namespace ConsoleAppCsharpComplete;
internal static class Program
{
private static async Task Main()
{
var cancellationTokenSource = new CancellationTokenSource(500);
string reversedWords;
try
{
reversedWords = await FsharpLibraryCompleteModule.wordReverser("one two three", true, cancellationTokenSource.Token);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
Console.WriteLine(reversedWords);
}
}
Enter fullscreen mode
Exit fullscreen mode
Console output
System.Exception: someAsyncFunction threw Exception
at FsharpLibraryComplete.FsharpLibraryCompleteModule.wordReverser@12-1.Invoke(Unit _arg1) in C:\projects\InteropPlayGround\Docker\ConsoleAppCsharpComplete\FsharpLibraryComplete\FsharpLibraryComplete.fs:line 13
at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvokeNoHijackCheck[a,b](AsyncActivation`1 ctxt, b result1, FSharpFunc`2 userCode) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 464
at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 104
--- End of stack trace from previous location ---
at ConsoleAppCsharpComplete.Program.Main() in C:\projects\InteropPlayGround\Docker\ConsoleAppCsharpComplete\ConsoleAppCsharpComplete\Program.cs:line 14
Unhandled exception. System.Exception: someAsyncFunction threw Exception
at FsharpLibraryComplete.FsharpLibraryCompleteModule.wordReverser@12-1.Invoke(Unit _arg1) in C:\projects\InteropPlayGround\Docker\ConsoleAppCsharpComplete\FsharpLibraryComplete\FsharpLibraryComplete.fs:line 13
at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvokeNoHijackCheck[a,b](AsyncActivation`1 ctxt, b result1, FSharpFunc`2 userCode) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 464
at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 104
--- End of stack trace from previous location ---
at ConsoleAppCsharpComplete.Program.Main() in C:\projects\InteropPlayGround\Docker\ConsoleAppCsharpComplete\ConsoleAppCsharpComplete\Program.cs:line 14
at ConsoleAppCsharpComplete.Program.<Main>()
Enter fullscreen mode
Exit fullscreen mode
Observations
Running the code as it is, will raise an exception from the F# library that gets caught by the C# application.
Please note that if the Task gets cancelled before raising the exception on the F# library, the C# application will catch the exception as System.Threading.Tasks.TaskCanceledException: A task was canceled.
Conclusion
It's not particularly difficult to use F# libraries in a C# application and vice versa, but there are a few things that we have to keep in mind, i.e. C# does not work with F# Async
, instead, we have to add additional steps to convert Async
into a Task
.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK