MixedFlowDirectionAnalyzer
Problem
The forward pipe (|>, ||>, |||>) and composition (>>) operators, and their backward counterparts (<|, <||, <|||, <<), are left-associative and share a precedence level, so they can be chained together in a single expression.
When both directions appear in the same chain, the reader has to start in the middle and work outwards two ways at once.
// Triggers analyzer
let add x y = x + y
let a b c = b |> add <| c
let wrap prefix suffix = prefix + suffix
let d items =
items
|> List.map string
|> String.concat ", "
|> wrap
<| "!"
let increment n = n + 1
let asText n = string n
let length (text: string) = text.Length
let composed = increment >> asText << length
Operator family does not matter, only direction. Every combination of a forward pipe or composition operator with a backward pipe or composition operator is reported:
// Triggers analyzer
let add3 x y z = x + y + z
let e x y z = (x, y) ||> add3 <| z
let f x y z = x |> add3 <|| (y, z)
let add x y = x + y
let g x y = add <|| (x, y) |> string
let createAdder x = fun y -> x + y
let parseNumber (text: string) = int text
let h x = x |> createAdder << parseNumber
Fix
Pick one direction. The following equivalent version uses only forward pipes and does not trigger the analyzer:
// Does not trigger analyzer
let wrap prefix suffix = prefix + suffix
let forwardPipeline items =
items
|> List.map string
|> String.concat ", "
|> fun joined -> wrap joined "!"
When the chain resists a single direction, bind the intermediate value with let:
// Does not trigger analyzer
let wrap prefix suffix = prefix + suffix
let boundPipeline items =
let joined =
items
|> List.map string
|> String.concat ", "
wrap joined "!"
A chain that runs in one direction is not reported, including a same-direction mix of pipe and composition operators. A backward pipe on its own stays available as the idiomatic way to drop a trailing parenthesis around a multi-line argument:
// Does not trigger analyzer
let h b = failwith <| sprintf "unexpected: %s" b
let add x y = x + y
let i x y = (x, y) ||> add |> string
let createAdder x = fun y -> x + y
let forwardMixed x y = (x |> createAdder >> string) y
Scope
Only operators on the same chain are compared. Parenthesizing an inner pipeline or composition makes it a separate expression. A flow operator nested inside a lambda or a parenthesized argument belongs to a different expression and is left alone:
// Does not trigger analyzer
let lambdaNestedPipeline items =
items
|> List.map (fun item -> string <| item + 1)
|> List.length
let parenthesizedInnerPipeline items =
ignore <| (items |> List.length)
let increment n = n + 1
let asText n = string n
let parenthesizedInnerComposition =
ignore <| (increment >> asText)
module List from Microsoft.FSharp.Collections
--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T member IsEmpty: bool member Item: index: int -> 'T with get ...
val string: value: 'T -> string
--------------------
type string = System.String
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
ionide-analyzers