memo.txt

教えていただいたこと、勉強したことのメモです。

openした結果モジュールが被ってしまった場合でも両方のモジュールの関数を扱える

この記事はF# Advent Calendar 2014 - connpassの23日目の記事です。
(日にち変更していただきました。ありがとうございます。)

昨日はむろほしりょうたさんの初心者がF#をUnityで使ってみた! - Qiitaでした。

F# のネタリスト - 2つのアンコールより、
「openした結果モジュールが被ってしまった場合でも両方のモジュールの関数を扱えること知りたいです」について書きます。

openした結果

MSDN

インポート宣言: open キーワード (F#)

F# コンパイラでは、
複数の開いているモジュールまたは名前空間に同じ名前が存在し、
あいまいさが生じる場合でも、エラーや警告を生成しません。 
あいまいさが生じた場合、F# では、
開いた時間が新しい方のモジュールまたは名前空間が優先されます。

これですね。

確認してみる

今回は「モジュールが被ってしまった場合」なので、同じ名前のmoduleを用意します。
関数名も同じにしておきました。

(* Tax.fs *)
namespace Advent.Tax

module Calc =
    let taxRate = 0.08
    let getPrice basePrice = basePrice * (1.0 + taxRate)

税率を指定して、getPriceに本体価格を渡すと税込価格を返してくれる。

(* Sale.fs *)
namespace Advent.Sale

module Calc =
    let discountRate = 0.2
    let getPrice basePrice = basePrice * (1.0 - discountRate)

値引率を指定して、getPriceに本体価格を渡すと値引き後の価格を返してくれる。

openします。

(* Display.fs *)
namespace Advent

open Advent.Tax
open Advent.Sale

module Display =
    let basePrice = 100.0
    let paymentPrice = Calc.getPrice basePrice

    printf "お支払金額 %A 円" paymentPrice

実行。

お支払金額 80.0 円

2割引のセール価格が表示されました。

open2行の上下をを入れ替えて実行。

(* Display.fs *)
namespace Advent

open Advent.Sale
open Advent.Tax

module Display =
    let basePrice = 100.0
    let paymentPrice = Calc.getPrice basePrice

    printf "お支払金額 %A 円" paymentPrice
お支払金額 108.0 円

税込価格が表示されました。

どちらも後で開いた方が優先されています。

両方のモジュールの関数を扱える

両方のモジュールの関数を使って、
セール価格にした後、消費税をのせた額をお支払金額をとして表示しましょう。

openのタイミングを変える

(* Display.fs *)
namespace Advent

open Advent.Sale

module Display =
    let basePrice = 100.0
    let salePrice = Calc.getPrice basePrice

    open Advent.Tax (* ここでopen *)
    let paymentPrice = Calc.getPrice salePrice
    
    printf "お支払金額 %A 円 (税抜価格 %A 円)" paymentPrice salePrice
お支払金額 86.4 円 (税抜価格 80.0 円)

セール価格にして、消費税をのせて、お支払金額にすることはできましたが、
両方の関数を扱えるという感じではありません。
Taxのopen前はSaleしか使えず、open後はTaxしか使えません。

フル修飾する

(* Display.fs *)
namespace Advent

module Display =
    let basePrice = 100.0
    let salePrice = Advent.Sale.Calc.getPrice basePrice
    let paymentPrice = Advent.Tax.Calc.getPrice salePrice
    
    printf "お支払金額 %A 円 (税抜価格 %A 円)" paymentPrice salePrice
お支払金額 86.4 円 (税抜価格 80.0 円)

できました。
1回しか書かないならこれでもいいかもしれません。

別名をつける

(* Display.fs *)
namespace Advent

module Display =
    module Sale = Advent.Sale.Calc
    module Tax = Advent.Tax.Calc    

    let basePrice = 100.0
    let salePrice = Sale.getPrice basePrice
    let paymentPrice = Tax.getPrice salePrice
    
    printf "お支払金額 %A 円 (税抜価格 %A 円)" paymentPrice salePrice
お支払金額 86.4 円 (税抜価格 80.0 円)

何度も使うならこれが便利そうですね。

ちなみに

名前が被っていない定義や関数は、特に何もしなくても両方扱えます。

Tax.fsとSale.fsに定義と関数を追加して確認します。

(* Tax.fs *)
namespace Advent.Tax

module Calc =
    let taxRate = 0.08
    let getPrice basePrice = basePrice * (1.0 + taxRate)
    (* 以下を追加 *)
    let oldTaxRate = 0.05
    let getOldPrice basePrice = basePrice * (1.0 + oldTaxRate)
(* Sale.fs *)
namespace Advent.Sale

module Calc =
    let discountRate = 0.2
    let getPrice basePrice = basePrice * (1.0 - discountRate)
    (* 以下を追加 *)
    let timesaleDiscountRate = 0.3
    let getTimeSalePrice basePrice = basePrice * (1.0 - timesaleDiscountRate)
(* Display.fs *)
namespace Advent

open System

open Advent.Sale
open Advent.Tax (* Taxをあとにしてまとめてopen *)

module Display =
    let basePrice = 100.0

    let toPercentage rate = rate * 100.0

    let paymentPrice = Calc.getPrice basePrice (* TaxのgetPrice *)
    let taxPer = Calc.taxRate |> toPercentage

    let oldPaymentPrice = Calc.getOldPrice basePrice
    let oldTaxPer = Calc.oldTaxRate |> toPercentage

    let timesalePrice = Calc.getTimeSalePrice basePrice
    let timesaleDiscountPer = Calc.timesaleDiscountRate |> toPercentage

    printf "現在のお支払金額 %A 円 (消費税 %A %)\n" paymentPrice taxPer
    printf "以前のお支払金額 %A 円 (消費税 %A %)\n" oldPaymentPrice oldTaxPer
    printf "タイムセール価格 %A 円 (%A %引)" timesalePrice timesaleDiscountPer
現在のお支払金額 108.0 円 (消費税 8.0 %)
以前のお支払金額 105.0 円 (消費税 5.0 %)
タイムセール価格 70.0 円 (30.0 %引)

ここまでのまとめ

  • 複数の開いているモジュール、名前空間に同じ名前が存在してもエラーにならない。
  • 上記の場合、新しく開いた方が優先される。
  • (openするタイミングをずらすか、)フル修飾するか、モジュールに別名をつけると両方の関数を扱える。
  • モジュール名が被っていても、内部の定義や関数の名前が被っていなければ、特別何もしなくても両方扱える。

既存モジュールを(擬似的に)拡張したり書き換えたりする

まとめを踏まえて。

既存モジュールと同名のモジュールを作ることで、
既存モジュールを拡張したり書き換えたりしたように見せられる!

ということを書いてみます。

(* StrEx.fs *)
namespace Advent.StrEx

(* Core.Stringの拡張,書き換え用Stringモジュールを作る *)
module String = 
    let greeting (str:string) = "Happy Holidays!"
    let length (str:string) = str.Length.ToString() + "文字"
(* Program.fs *)
namespace Advent

open Advent.StrEx (* Core.Stringは暗黙で開かれるのでこれが後になる *)

module Execute =
    let baseString = "aaa"

    (* 自作関数greetingをString.greetingで使える *)
    let greetingString = String.greeting baseString
    printf "%A\n" greetingString

    (* Core.Stringの関数も使える *)
    let strings = [baseString; "bbb"]
    let concatString = String.concat "," strings
    printf "%A\n" concatString

    (* Core.StringのlengthではなくStrExのlengthが使える *)
    let length = String.length baseString
    printf "%A" length
"Happy Holidays!"
"aaa,bbb"
"3文字"

まとめ

  • 複数の開いているモジュール、名前空間に同じ名前が存在してもエラーにならない。
  • 上記の場合、新しく開いた方が優先される。
  • (openするタイミングをずらすか、)フル修飾するか、モジュールに別名をつけると両方の関数を扱える。
  • モジュール名が被っていても、内部の定義や関数の名前が被っていなければ、特別何もしなくても両方扱える。
  • 既存モジュールの(擬似的な)拡張や書き換えが気軽にできる