You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/fsharp/style-guide/conventions.md
+116-9Lines changed: 116 additions & 9 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,7 +1,7 @@
1
1
---
2
2
title: F# coding conventions
3
3
description: Learn general guidelines and idioms when writing F# code.
4
-
ms.date: 11/04/2019
4
+
ms.date: 01/15/2020
5
5
---
6
6
# F# coding conventions
7
7
@@ -42,7 +42,7 @@ type MyClass() =
42
42
43
43
### Carefully apply `[<AutoOpen>]`
44
44
45
-
The `[<AutoOpen>]` construct can pollute the scope of what is available to callers, and the answer to where something comes from is "magic". This is generally not a good thing. An exception to this rule is the F# Core Library itself (though this fact is also a bit controversial).
45
+
The `[<AutoOpen>]` construct can pollute the scope of what is available to callers, and the answer to where something comes from is "magic". This is not a good thing. An exception to this rule is the F# Core Library itself (though this fact is also a bit controversial).
46
46
47
47
However, it is a convenience if you have helper functionality for a public API that you wish to organize separately from that public API.
48
48
@@ -85,7 +85,7 @@ let parsed = StringTokenization.parse s // Must qualify to use 'parse'
85
85
86
86
In F#, the order of declarations matters, including with `open` statements. This is unlike C#, where the effect of `using` and `using static` is independent of the ordering of those statements in a file.
87
87
88
-
In F#, elements opened into a scope can shadow others already present. This means that reordering `open` statements could alter the meaning of code. As a result, any arbitrary sorting of all `open` statements (for example, alphanumerically) is generally not recommended, lest you generate different behavior that you might expect.
88
+
In F#, elements opened into a scope can shadow others already present. This means that reordering `open` statements could alter the meaning of code. As a result, any arbitrary sorting of all `open` statements (for example, alphanumerically) is not recommended, lest you generate different behavior that you might expect.
89
89
90
90
Instead, we recommend that you sort them [topologically](https://en.wikipedia.org/wiki/Topological_sorting); that is, order your `open` statements in the order in which _layers_ of your system are defined. Doing alphanumeric sorting within different topological layers may also be considered.
91
91
@@ -314,7 +314,7 @@ Types such as `Result<'Success, 'Error>` are appropriate for basic operations wh
314
314
315
315
## Partial application and point-free programming
316
316
317
-
F# supports partial application, and thus, various ways to program in a point-free style. This can be beneficial for code reuse within a module or the implementation of something, but it is generally not something to expose publicly. In general, point-free programming is not a virtue in and of itself, and can add a significant cognitive barrier for people who are not immersed in the style.
317
+
F# supports partial application, and thus, various ways to program in a point-free style. This can be beneficial for code reuse within a module or the implementation of something, but it is not something to expose publicly. In general, point-free programming is not a virtue in and of itself, and can add a significant cognitive barrier for people who are not immersed in the style.
318
318
319
319
### Do not use partial application and currying in public APIs
320
320
@@ -437,11 +437,118 @@ Finally, automatic generalization is not always a boon for people who are new to
437
437
438
438
## Performance
439
439
440
+
### Prefer structs for small data types
441
+
442
+
Using structs (also called Value Types) can often result in higher performance for some code because it typically avoids allocating objects. However, structs are not always a "go faster" button: if the size of the data in a struct exceeds 16 bytes, copying the data can often result in more CPU time spend than using a reference type.
443
+
444
+
To determine if you should use a struct, consider the following conditions:
445
+
446
+
- If the size of your data is 16 bytes or smaller.
447
+
- If you're likely to have many of these data types resident in memory in a running program.
448
+
449
+
If the first condition applies, you should generally use a struct. If both apply, you should almost always use a struct. There may be some cases where the previous conditions apply, but using a struct is no better or worse than using a reference type, but they are likely to be rare. It's important to always measure when making changes like this, though, and not operate on assumption or intuition.
450
+
451
+
#### Prefer struct tuples when grouping small value types
452
+
453
+
Consider the following two functions:
454
+
455
+
```fsharp
456
+
let rec runWithTuple t offset times =
457
+
let offsetValues x y z offset =
458
+
(x + offset, y + offset, z + offset)
459
+
460
+
if times <= 0 then
461
+
t
462
+
else
463
+
let (x, y, z) = t
464
+
let r = offsetValues x y z offset
465
+
runWithTuple r offset (times - 1)
466
+
467
+
let rec runWithStructTuple t offset times =
468
+
let offsetValues x y z offset =
469
+
struct(x + offset, y + offset, z + offset)
470
+
471
+
if times <= 0 then
472
+
t
473
+
else
474
+
let struct(x, y, z) = t
475
+
let r = offsetValues x y z offset
476
+
runWithStructTuple r offset (times - 1)
477
+
```
478
+
479
+
When you benchmark these functions with a statistical benchmarking tool like [BenchmarkDotNet](https://benchmarkdotnet.org/), you'll find that the `runWithStructTuple` function that uses struct tuples runs 40% faster and allocates no memory.
480
+
481
+
However, these results won't always be the case in your own code. If you mark a function as `inline`, code that uses reference tuples may get some additional optimizations, or code that would allocate could simply be optimized away. You should always measure results whenever performance is concerned, and never operate based on assumption or intuition.
482
+
483
+
#### Prefer struct records when the data type is small
484
+
485
+
The rule of thumb described earlier also holds for [F# record types](../language-reference/records.md). Consider the following data types and functions that process them:
486
+
487
+
```fsharp
488
+
type Point = { X: float; Y: float; Z: float }
489
+
490
+
[<Struct>]
491
+
type SPoint = { X: float; Y: float; Z: float }
492
+
493
+
let rec processPoint (p: Point) offset times =
494
+
let inline offsetValues (p: Point) offset =
495
+
{ p with X = p.X + offset; Y = p.Y + offset; Z = p.Z + offset }
496
+
497
+
if times <= 0 then
498
+
p
499
+
else
500
+
let r = offsetValues p offset
501
+
processPoint r offset (times - 1)
502
+
503
+
let rec processStructPoint (p: SPoint) offset times =
504
+
let inline offsetValues (p: SPoint) offset =
505
+
{ p with X = p.X + offset; Y = p.Y + offset; Z = p.Z + offset }
506
+
507
+
if times <= 0 then
508
+
p
509
+
else
510
+
let r = offsetValues p offset
511
+
processStructPoint r offset (times - 1)
512
+
```
513
+
514
+
This is similar to the previous tuple code, but this time the example uses records and an inlined inner function.
515
+
516
+
When you benchmark these functions with a statistical benchmarking tool like [BenchmarkDotNet](https://benchmarkdotnet.org/), you'll find that `processStructPoint` runs nearly 60% faster and allocates nothing on the managed heap.
517
+
518
+
#### Prefer struct discriminated unions when the data type is small
519
+
520
+
The previous observations about performance with struct tuples and records also holds for [F# Discriminated Unions](../language-reference/discriminated-unions.md). Consider the following code:
521
+
522
+
```fsharp
523
+
type Name = Name of string
524
+
525
+
[<Struct>]
526
+
type SName = SName of string
527
+
528
+
let reverseName (Name s) =
529
+
s.ToCharArray()
530
+
|> Array.rev
531
+
|> string
532
+
|> Name
533
+
534
+
let structReverseName (SName s) =
535
+
s.ToCharArray()
536
+
|> Array.rev
537
+
|> string
538
+
|> SName
539
+
```
540
+
541
+
It's common to define single-case Discriminated Unions like this for domain modeling. When you benchmark these functions with a statistical benchmarking tool like [BenchmarkDotNet](https://benchmarkdotnet.org/), you'll find that `structReverseName` runs about 25% faster than `reverseName` for small strings. For large strings, both perform about the same. So, in this case, it's always preferable to use a struct. As previously mentioned, always measure and do not operate on assumptions or intuition.
542
+
543
+
Although the previous example showed that a struct Discriminated Union yielded better performance, it is common to have larger Discriminated Unions when modeling a domain. Larger data types like that may not perform as well if they are structs depending on the operations on them, since more copying could be involved.
544
+
545
+
### Functional programming and mutation
546
+
440
547
F# values are immutable by default, which allows you to avoid certain classes of bugs (especially those involving concurrency and parallelism). However, in certain cases, in order to achieve optimal (or even reasonable) efficiency of execution time or memory allocations, a span of work may best be implemented by using in-place mutation of state. This is possible in an opt-in basis with F# with the `mutable` keyword.
441
548
442
-
However, use of `mutable` in F# may feel at odds with functional purity. This is fine, if you adjust expectations from purity to [referential transparency](https://en.wikipedia.org/wiki/Referential_transparency). Referential transparency - not purity - is the end goal when writing F# functions. This allows you to write a functional interface over a mutation-based implementation for performancecritical code.
549
+
Use of `mutable` in F# may feel at odds with functional purity. This is understandable, but functional purity everywhere can be at odds with performance goals. A compromise is to encapsulate mutation such that callers need not care about what happens when they call a function. This allows you to write a functional interface over a mutation-based implementation for performance-critical code.
443
550
444
-
### Wrap mutable code in immutable interfaces
551
+
####Wrap mutable code in immutable interfaces
445
552
446
553
With referential transparency as a goal, it is critical to write code that does not expose the mutable underbelly of performance-critical functions. For example, the following code implements the `Array.contains` function in the F# core library:
447
554
@@ -459,7 +566,7 @@ let inline contains value (array:'T[]) =
459
566
460
567
Calling this function multiple times does not change the underlying array, nor does it require you to maintain any mutable state in consuming it. It is referentially transparent, even though almost every line of code within it uses mutation.
461
568
462
-
### Consider encapsulating mutable data in classes
569
+
####Consider encapsulating mutable data in classes
463
570
464
571
The previous example used a single function to encapsulate operations using mutable data. This is not always sufficient for more complex sets of data. Consider the following sets of functions:
465
572
@@ -505,9 +612,9 @@ type Closure1Table() =
505
612
506
613
`Closure1Table` encapsulates the underlying mutation-based data structure, thereby not forcing callers to maintain the underlying data structure. Classes are a powerful way to encapsulate data and routines that are mutation-based without exposing the details to callers.
507
614
508
-
### Prefer `let mutable` to reference cells
615
+
####Prefer `let mutable` to reference cells
509
616
510
-
Reference cells are a way to represent the reference to a value rather than the value itself. Although they can be used for performance-critical code, they are generally not recommended. Consider the following example:
617
+
Reference cells are a way to represent the reference to a value rather than the value itself. Although they can be used for performance-critical code, they are not recommended. Consider the following example:
0 commit comments