基本的には、Option.mapでOKです。

utop # Some 10 |> Option.map (fun v -> v * 2) |> Option.map (fun v -> v - 1);;
- : int option = Some 19

ただし、途中でNoneを返すような関数があると、コンパイルエラーになります。

utop # Some 10 |> Option.map (fun v -> None) |> Option.map (fun v -> v - 1);;
Line 1, characters 41-68:
Error: This expression has type int option -> int option
       but an expression was expected of type 'a option option -> 'b
       Type int is not compatible with type 'a option 

Option.mapは結果をOptionに包んで返してくれますが、自分でNoneを返してしまっているので、結果が'a option optionとなってしまって、型が合わない状態になっています。つまりOptionの入れ子状態です。

余談ですが、コンパイル段階でこういった問題が発覚するのは本当に素敵ですね!

回避策1 join

Option.joinは、a option optionという入れ子になったOptionを一個のみにしてくれる関数です。

utop # Some (Some 10) |> Option.join;;
- : int option = Some 10
utop # Some (None) |> Option.join;;
- : 'a option = None

コレを使えば、以下のようにできます。

utop # Some 10 |> Option.map (fun v -> Some (v * 2)) |> Option.join |> Option.map (fun v -> v - 1);;
- : int option = Some 19

utop # Some 10 |> Option.map (fun v -> None) |> Option.join |> Option.map (fun v -> v - 1);;
- : int option = None

回避策2 bind

こちらはElmでいうandThen関数に相当します。

Option.mapと違って、渡す関数では結果を自分でOptionに包んで返してあげる必要があります。

utop # Option.bind (Some 10) (fun v -> Some (v * 2));;
- : int option = Some 20
utop # Option.bind (Some 10) (fun v -> None);;
- : 'a option = None

しかし、よく見ると引数の順番がOption.mapOption.bindでは違います。

utop # Option.map;;
- : ('a -> 'b) -> 'a option -> 'b option = <fun>

utop # Option.bind;;
- : 'a option -> ('a -> 'b option) -> 'b option = <fun>

Option.bindの方では、第1引数に処理対象のOption、そして第2引数に処理内容となる関数を渡すようになっています。
そのため、パイプライン演算子|>を使って処理対象となる値を渡してチェインしていく、ということができません。

そこで、Option.bindをラップする中間演算子を宣言してあげると記述が楽になります。

utop # let (>>=) v f = Option.bind v f;;
val ( >>= ) : 'a option -> ('a -> 'b option) -> 'b option = <fun>

使い方は以下のようになります。

utop # Some 10 >>= (fun v -> Some(v * 2)) >>= (fun v -> Some (v - 1));;
- : int option = Some 19

utop # Some 10 >>= (fun v -> None) >>= (fun v -> Some (v - 1));;
- : int option = None

ちなみに、>>=は、Lwt.bindでも利用されているので、もしLwtを利用する際には被らないように別のものにする必要が有あります。

まとめ

基本的にはOption.joinよりはOption.bindを利用して、さらにそのために中間演算子を定義してしまうのがベターだと思います。

ちなみに、当然中間演算子にしなくても普通の関数でもOKです。
普通の関数として宣言しておけばパイプライン演算子|>でチェインしていくことができます。

utop # let and_then f v = Option.bind v f;;
val and_then : ('a -> 'b option) -> 'a option -> 'b option = <fun>

utop # Some 10 |> and_then (fun v -> Some v) |> and_then (fun v -> Some (v * 2));;
- : int option = Some 20
utop # Some 10 |> and_then (fun v -> None) |> and_then (fun v -> Some (v * 2));;
- : int option = None

とはいえ、コレなら素直に中間演算子として定義しておいてあげたほうが楽ちんだと思います。