I've covered the motivation behind the usage of Span<T> in my
blog. To make a long story short, it's an easy way to reduce some heap allocations without sacrificing code readability.
At some point, I've decided to check how Span<T> is supported in F# which I'm
a huge believer in.
In the example code, I've covered the conversion of Linux permissions into octal representation. Here's the code to recap what's happening
- internal ref struct SymbolicPermission
- {
- private struct PermissionInfo
- {
- public int Value { get; set; }
- public char Symbol { get; set; }
- }
-
- private const int BlockCount = 3;
- private const int BlockLength = 3;
- private const int MissingPermissionSymbol = '-';
-
-
-
-
-
- private readonly static Dictionary<int, PermissionInfo> Permissions = new Dictionary<int, PermissionInfo>() {
- {0, new PermissionInfo {
- Symbol = 'r',
- Value = 4
- } },
- {1, new PermissionInfo {
- Symbol = 'w',
- Value = 2
- }},
- {2, new PermissionInfo {
- Symbol = 'x',
- Value = 1
- }}};
-
- private ReadOnlySpan<char> _value;
-
- private SymbolicPermission(string value)
- {
- _value = value;
- }
-
- public static SymbolicPermission Parse(string input)
- {
- if (input.Length != BlockCount * BlockLength)
- {
- throw new ArgumentException("input should be a string 3 blocks of 3 characters each");
- }
- for (var i = 0; i < input.Length; i++)
- {
- TestCharForValidity(input, i);
- }
-
- return new SymbolicPermission(input);
- }
-
- public int GetOctalRepresentation()
- {
- var res = 0;
- for (var i = 0; i < BlockCount; i++)
- {
- var block = GetBlock(i);
- res += ConvertBlockToOctal(block) * (int)Math.Pow(10, BlockCount - i - 1);
- }
- return res;
- }
-
- private static void TestCharForValidity(string input, int position)
- {
- var index = position % BlockLength;
- var expectedPermission = Permissions[index];
- var symbolToTest = input[position];
- if (symbolToTest != expectedPermission.Symbol && symbolToTest != MissingPermissionSymbol)
- {
- throw new ArgumentException($"invalid input in position {position}");
- }
- }
-
- private ReadOnlySpan<char> GetBlock(int blockNumber)
- {
- return _value.Slice(blockNumber * BlockLength, BlockLength);
- }
-
- private int ConvertBlockToOctal(ReadOnlySpan<char> block)
- {
- var res = 0;
- foreach (var (index, permission) in Permissions)
- {
- var actualValue = block[index];
- if (actualValue == permission.Symbol)
- {
- res += permission.Value;
- }
- }
-
-
-
-
- return res;
- }
- }
-
- public static class SymbolicUtils
- {
- public static int SymbolicToOctal(string input)
- {
- var permission = SymbolicPermission.Parse(input);
- return permission.GetOctalRepresentation();
- }
- }
Which is called like this:
- var result = SymbolicUtils.SymbolicToOctal("rwxr-x-w-");
So let's now jump to F#. We'll declare Helpers type which will calculate octal representation instead of C# code.
- [<Struct>]
- type PermissionInfo(symbol: char, value: int) =
- member x.Symbol = symbol
- member x.Value = value
-
- type Helpers =
- val private Permissions : PermissionInfo[]
- new () = {
- Permissions =
- [|PermissionInfo('r', 4);
- PermissionInfo('w', 2);
- PermissionInfo('x', 1); |]
- }
-
- member x.ConvertBlockToOctal (block : ReadOnlySpan<char>) =
- let mutable acc = 0
- for i = 0 to x.Permissions.Length - 1 do
- if block.[i] = x.Permissions.[i].Symbol then
- acc <- acc + x.Permissions.[i].Value
- else
- acc <- acc
- acc
One notable point here is that Permissions array is marked as val. As
documentation states it allows declaring a location to store a value in a class or structure type, without initializing it.
Calling it in C# is seamless.
- var block = GetBlock(i);
- res += new Helpers().ConvertBlockToOctal(block) * (int)Math.Pow(10, BlockCount - i - 1);
Here are the benchmarks
Although the F# version allocates more memory execution, the time difference is quite impressive.
So as we can see F# keeps up with C# and supports new features quite well.