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
  1. internal ref struct SymbolicPermission
  2. {
  3. private struct PermissionInfo
  4. {
  5. public int Value { get; set; }
  6. public char Symbol { get; set; }
  7. }
  8. private const int BlockCount = 3;
  9. private const int BlockLength = 3;
  10. private const int MissingPermissionSymbol = '-';
  11. //I could smh decuce value from the position
  12. //Since values are powers of 2
  13. //But I think that such dictionary
  14. //Allows better to capture domain knowledge
  15. private readonly static Dictionary<int, PermissionInfo> Permissions = new Dictionary<int, PermissionInfo>() {
  16. {0, new PermissionInfo {
  17. Symbol = 'r',
  18. Value = 4
  19. } },
  20. {1, new PermissionInfo {
  21. Symbol = 'w',
  22. Value = 2
  23. }},
  24. {2, new PermissionInfo {
  25. Symbol = 'x',
  26. Value = 1
  27. }}};
  28. private ReadOnlySpan<char> _value;
  29. private SymbolicPermission(string value)
  30. {
  31. _value = value;
  32. }
  33. public static SymbolicPermission Parse(string input)
  34. {
  35. if (input.Length != BlockCount * BlockLength)
  36. {
  37. throw new ArgumentException("input should be a string 3 blocks of 3 characters each");
  38. }
  39. for (var i = 0; i < input.Length; i++)
  40. {
  41. TestCharForValidity(input, i);
  42. }
  43. return new SymbolicPermission(input);
  44. }
  45. public int GetOctalRepresentation()
  46. {
  47. var res = 0;
  48. for (var i = 0; i < BlockCount; i++)
  49. {
  50. var block = GetBlock(i);
  51. res += ConvertBlockToOctal(block) * (int)Math.Pow(10, BlockCount - i - 1);
  52. }
  53. return res;
  54. }
  55. private static void TestCharForValidity(string input, int position)
  56. {
  57. var index = position % BlockLength;
  58. var expectedPermission = Permissions[index];
  59. var symbolToTest = input[position];
  60. if (symbolToTest != expectedPermission.Symbol && symbolToTest != MissingPermissionSymbol)
  61. {
  62. throw new ArgumentException($"invalid input in position {position}");
  63. }
  64. }
  65. private ReadOnlySpan<char> GetBlock(int blockNumber)
  66. {
  67. return _value.Slice(blockNumber * BlockLength, BlockLength);
  68. }
  69. private int ConvertBlockToOctal(ReadOnlySpan<char> block)
  70. {
  71. var res = 0;
  72. foreach (var (index, permission) in Permissions)
  73. {
  74. var actualValue = block[index];
  75. if (actualValue == permission.Symbol)
  76. {
  77. res += permission.Value;
  78. }
  79. }
  80. //I guess name of the method suggests
  81. //that I shoud play with base of 8
  82. //but since requirements don't explictly state that
  83. //I think base of 10 is good enough :)
  84. return res;
  85. }
  86. }
  87. public static class SymbolicUtils
  88. {
  89. public static int SymbolicToOctal(string input)
  90. {
  91. var permission = SymbolicPermission.Parse(input);
  92. return permission.GetOctalRepresentation();
  93. }
  94. }
Which is called like this:
  1. 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.
  1. [<Struct>]
  2. type PermissionInfo(symbol: char, value: int) =
  3. member x.Symbol = symbol
  4. member x.Value = value
  5. type Helpers =
  6. val private Permissions : PermissionInfo[]
  7. new () = {
  8. Permissions =
  9. [|PermissionInfo('r', 4);
  10. PermissionInfo('w', 2);
  11. PermissionInfo('x', 1); |]
  12. }
  13. member x.ConvertBlockToOctal (block : ReadOnlySpan<char>) =
  14. let mutable acc = 0
  15. for i = 0 to x.Permissions.Length - 1 do
  16. if block.[i] = x.Permissions.[i].Symbol then
  17. acc <- acc + x.Permissions.[i].Value
  18. else
  19. acc <- acc
  20. 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.
  1. var block = GetBlock(i);
  2. res += new Helpers().ConvertBlockToOctal(block) * (int)Math.Pow(10, BlockCount - i - 1);
Here are the benchmarks
Using Span<T /> In F#
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.