15
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

F#Advent Calendar 2020

Day 1

F# 5.0の新機能

Last updated at Posted at 2020-11-30

はじめに

2020/11/11(水)にリリースされた F# 5.0 の新機能について簡単に紹介します。
抜け漏れ誤りがあったらごめんなさい🙇

※現在まだプレビューの機能については触れません

F# 5.0 新機能

F#スクリプトでのNugetパッケージ参照

今までF#スクリプトではNugetパッケージを直接参照することができませんでした。
それがF# 5.0から可能になりました。

#r "nuget:Flavedo.Zest"
open Flavedo.Zest
// do something

詳しくは こちらの記事 を参照してください

String Interpolation

RFC FS-1001

C# でいうところの 挿入文字列リテラル が F# にも追加されました。
これは文字列リテラル中に値を挿入できるというものです。

定義
$"文字列 {挿入したい値} "
サンプル
let name = "Midoliy"
let age = 31
let message = $"{name} is {age} years old."
printfn "%s" message

Midoliy is 31 years old.

また、sprintf関数 と同様にフォーマット指定子を利用することで挿入する値の型を指定することが可能です。
フォーマット指定子を利用することで printf関数sprintf関数 と同様、コンパイル時に意図しない型が挿入されることを防ぐことができます。

定義
$"文字列 フォーマット指定子{挿入したい値} "
サンプル
let name = "Midoliy"
let age = 31
let message = $"%s{name} is %d{age} years old."
printfn "%s" message

Midoliy is 31 years old.

nameof

RFC FS-1003

C# の nameof式 と同様のものが F# にも追加されました。
nameofを使うことで変数・型・メンバーなどの名前が文字列定数として生成されます。

定義
nameof 対象
サンプル
let f x = nameof x
let y = 10

printfn "%s" (f y)
printfn "%s" (nameof y)
printfn "%s" (nameof int)

x
y
int

この機能はいろいろな利用シーンがありますが、例えば WPF のバインディングに必須な INotifyPropertyChanged を実装する際、文字列リテラルを直指定ではなく C# と同様に nameof式 で指定することが可能になったりします。

サンプル
open System.ComponentModel

type Rectangle () =
    let mutable height = 0f
    let mutable width = 0f
    let propertyChanged = Event<_, _>()

    interface System.ComponentModel.INotifyPropertyChanged with
        [<CLIEventAttribute>]
        member __.PropertyChanged = propertyChanged.Publish

    member __.Height
        with get() = height
        and set(value) = 
            height <- value
            propertyChanged.Trigger(__, PropertyChangedEventArgs(nameof height))
            
    member __.Width
        with get() = width
        and set(value) = 
            width <- value
            propertyChanged.Trigger(__, PropertyChangedEventArgs(nameof width))

    member __.Area with get() =
        height * width

Open Type declarations

RFC FS-1068

C# でいうところの using static ディレクティブ に似た機能が F# にも追加されました。
Open Type declarations は型名を指定せずに静的メンバにアクセスできるだけでなく、特定のモジュール内にある判別共用体やenum、レコードなどにアクセスすることが可能となっています。

定義
open type 型名
サンプル1
open type System.Math

let x = Min(1.0, 2.0)
printfn $"%f{x}"

1.000000

サンプル2
module M =
    type Foo = { X:int; Y:int; }
    type Bar = { X:int; Y:int; }

// open type を利用した場合
open type M.Foo

let foo = { X = 10; Y = 20; }
printfn $"%A{foo} : %A{foo.GetType()}"

{ X = 10; Y = 20 } : Program+M+Foo

サンプル3
module M =
    type Foo = { X:int; Y:int; }
    type Bar = { X:int; Y:int; }

// 通常の open を利用した場合
open M

let v = { X = 10; Y = 20; }
printfn $"%A{v} : %A{v.GetType()}"

{ X = 10; Y = 20 } : Program+M+Bar

Sliceの機能強化

組み込みデータ型に対してスライスした際の挙動を統一

RFC FS-1077

F#5以前
let list = [ 0..10 ]
let array = [| 0..10 |]
let str = "hello!"

list.[-2..(-1)] |> printfn("%A")
array.[-2..(-1)] |> printfn("%A")
str.[-2..(-1)] |> printfn("%A")

[ ]
throw exception
throw exception

F#5
let list = [ 0..10 ]
let array = [| 0..10 |]
let str = "hello!"

list.[-2..(-1)] |> printfn("%A")
array.[-2..(-1)] |> printfn("%A")
str.[-2..(-1)] |> printfn("%A")

[ ]
[| |]
""

3次元・4次元配列に対するスライスの強化

RFC FS-1077

以下のような3次元配列を考えます。

z=0

xy 0 1
0 0 1
1 2 3

z=1

xy 0 1
0 4 5
1 6 7

このときの y = 0, z = 1x行 = 4, 5 をスライスで簡単に取得できるようになりました。

サンプル
let dim = 2
let m = Array3D.zeroCreate<int> dim dim dim

let mutable cnt = 0

for z in 0..dim-1 do
    for y in 0..dim-1 do
        for x in 0..dim-1 do
            m.[x,y,z] <- cnt
            cnt <- cnt + 1

// F# 5.0 から以下のようなスライスが可能に!
m.[*, 0, 1]
|> printfn "%A"

[| 4; 5 |]

F# Quotationsの機能改善

RFC FS-1071

F# Quotationsは強力な機能ですが、SRTP制約が課されている場合などでそのセマンティクスを十分に表すような呼び出しが行われていませんでした。
F# 5.0となりその機能が改善されました。

サンプル
open FSharp.Linq.RuntimeHelpers

let eval q = LeafExpressionConverter.EvaluateQuotation q

let inline negate x = -x
// negateは以下のようなシグニチャを持つ関数です。
//    val inline negate: x: ^a ->  ^a when  ^a : (static member ( ~- ) :  ^a ->  ^a)


<@ negate 1.0 @>  |> eval
// 上記のコードはnegateに課せられているSRTP制約を守っていますが、以前のバージョンでは例外が発生していました。
// F# 5.0からはこのコードが有効となります。

Applicative Computation Expressions

RFC FS-1063

「すべての処理が独立」しており「その結果を蓄積させる」だけのコンピュテーション式を作成する場合に、より高パフォーマンスとなる機能が追加されました。
この機能はライブラリ作成者向けの機能ですが、利用者側にとっても今までのコンピュテーション式となんら変わらない使い心地のままパフォーマンスが上がるので両者にメリットがある機能アップです。

大きく分けると "BindReturn""MergeSources" の2種類が追加されました。

BindReturn

F# 4.7 までは let!return を別々に定義していましたが、これを1つにまとめたようなものが BindReturn です。

F#4.7まで
type OptionBuilder() =
  member _.Bind(x, f) = Option.bind f x
  member _.Return(x) = x
  
let option = OptionBuilder()
  
let r : int option = 
  option {
    let! a = Some 10
    return a
  }
  
printfn $"%A{r}"

Some 10

F#5.0から
type OptionBuilder() =
  member _.BindReturn(x, f) = Option.map f x
  
let option = OptionBuilder()
  
let r : int option = 
  option {
    let! a = Some 10
    return a
  }
  
printfn $"%A{r}"

Some 10

厳密には上記のコードは同じではありません。
たとえば、BindReturn のみを定義した場合には2個目の let! を利用することはできません。

BindReturnのみの定義ではダメな例
let r : int option =
  option {
    let! a = Some 10
    let! b = Some 20  // コレはダメ!Bindを定義する必要あり
    return a + b
  }

MergeSources

「独立した処理」を表現するための構文が BindReturn であるならば、「結果を蓄積する」ための構文が MergeSources になります。
結果を蓄積させるために and! を利用する必要があることに注意してください。

F#5.0から
type OptionBuilder() =
  member _.BindReturn(x, f) = Option.map f x
  member _.MergeSources(x, y) = (x, y) ||> Option.map2 (fun x y -> (x, y))

let option = OptionBuilder()

let r : int option = 
  option {
    let! a = Some 10
    and! b = Some 20
    return a + b
  }

printfn $"%A{r}"

let r2 : int option = 
  option {
    let! a = Some 10
    and! b = None
    return a + b
  }

printfn $"%A{r2}"

Some 30
None

.NET相互運用の改善

C# に追加されているインターフェースのデフォルト実装への対応やnull許容型との相互運用性の向上などが行われました。

異なる型でのジェネリックインターフェース実装

RFC FS-1031

異なる型で同じインターフェースを実装できるようになりました。

type IFoo<'T> =
  abstract member Function : 'T -> 'T

type Foo() =
  interface IFoo<int> with
    member _.Function x = x + x

  interface IFoo<string> with
    member _.Function s = $"string: %s{s}"

let foo = Foo()
printfn $"%d{ (foo :> IFoo<int>).Function(10) }"
printfn $"""%s{ (foo :> IFoo<string>).Function("abc") }"""

20
string: abc

インターフェースのデフォルト実装

RFC FS-1074

C# で書かれたインターフェースにデフォルト実装がなされている場合に、F# でもそのデフォルト実装されたものを呼び出すことができるようになりました。

C#側
using System;

namespace Sample
{
    public interface IFoo
    {
        int Value => 100;
    }
}
F#側
open Sample

type Foo() =
  interface IFoo

let foo = Foo() :> IFoo
printfn $"%d{foo.Value}"

100

null許容型との相互運用性の改善

RFC FS-1075

今まで Nullable でラップしなければならなかったところが必要なくなりました。
特に F# から C# のライブラリを利用する場合に便利になります。

C#側
using System;

namespace Sample
{
    public class Foo
    {
        public int Function(int? x)
            => (x ?? 0) * 100;
    }
}
F#側
open Sample

let foo = Foo()
// Nullable<int> でラップしなくても良い
let x = foo.Function(100)
printfn $"%d{x}"

10000

その他

float32

RFC FS-1080

今まではfloat32型を定義する場合に "." が必要でしたが、F# 5.0からはサフィックスのみでよくなりました。

F#5以前
let x = 123.f
F#5
let x = 123f

uint32

RFC FS-1082

int32 は int と省略できますが、uint32 は uint と省略できませんでした。
F# 5.0からはそれが可能となりました。

F#5以前
let f (x:uint32) = x * 10
F#5
let f (x:uint) = x * 10

おわりに

この他にも現在プレビュー中の機能があったりして、今後も目が離せないですね!
今回の F# 5.0 に関するアナウンスは ココで 確認すると良いと思います。

15
4
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?