Header menu logo ionide-analyzers

MixedPipeDirectionAnalyzer

Problem

The forward pipes (|>, ||>, |||>) and the backward pipes (<|, <||, <|||) 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
    <| "!"

Arity does not matter, only direction. Every combination of a forward pipe with a backward pipe 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

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, whatever mix of arities it uses. 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

Scope

Only operators on the same chain are compared. Parenthesizing an inner pipeline makes it a separate expression. A pipe 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)
val add: x: int -> y: int -> int
val x: int
val y: int
val a: b: int -> c: int -> int
val b: int
val c: int
val wrap: prefix: string -> suffix: string -> string
val prefix: string
val suffix: string
val d: items: 'a list -> string
val items: 'a list
Multiple items
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 map: mapping: ('T -> 'U) -> list: 'T list -> 'U list
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
module String from Microsoft.FSharp.Core
val concat: sep: string -> strings: string seq -> string
val add3: x: int -> y: int -> z: int -> int
val z: int
val e: x: int -> y: int -> z: int -> int
val f: x: int -> y: int -> z: int -> int
val g: x: int -> y: int -> string
val forwardPipeline: items: 'a list -> string
val joined: string
val boundPipeline: items: 'a list -> string
val h: b: string -> 'a
val b: string
val failwith: message: string -> 'T
val sprintf: format: Printf.StringFormat<'T> -> 'T
val i: x: int -> y: int -> string
val lambdaNestedPipeline: items: int list -> int
val items: int list
val item: int
val length: list: 'T list -> int
val parenthesizedInnerPipeline: items: 'a list -> unit
val ignore: value: 'T -> unit

Type something to start searching.