0
0

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 3 years have passed since last update.

【Swift】ミュータブルな要素を操作をせずに配列をDictionaryに変換する

Last updated at Posted at 2020-03-02

はじめに

列挙可能なデータからDictionaryをつくりたくなることはないでしょうか。
私はありました。
私はC#を以前に学んでおり、C#にはIEnumerable.ToDictionary()という拡張メソッドがあったので、
Swiftにも同じ機能を持った関数がないか調べていました。

StackOverflowで見つかったサンプル

let myDictionary = myArray.reduce([Int: String]()) { (dict, person) -> [Int: String] in
    var dict = dict
    dict[person.position] = person.name
    return dict
}

なるほど、reduce()の初期値に空のDictionaryのインスタンスを与えて、
配列の要素順にDictionaryインスタンスに要素を追加していけば完成する。

reduce()は畳み込んでデータの次元を落とす処理をするイメージだったので、
こんなこともできるのかと目から鱗でした。

しかし気になることが一つ。
var dict = dict
キミの存在だ。

せっかくの関数型要素を強くもつ言語なのだから
イミュータブルなデータの操作のみでできないものだろうか。 というのが以降書いている内容の動機です。
(標準ライブラリの中は除くとして)

ついでにC#にあるようなtoDictionary()拡張を用意することで、
簡単に書けるようにしてしまおうという目論みです。

そして毎度おなじみ。

もっといいものが世の中にはあるらしい。
自己流の答えを書く前に世の中で正しいとされている方法を先に紹介です。
新しいreduceによる解決策

users.reduce(into: [Int: User]()) { $0[$1.id] = $1 }

intoラベルを付けた場合、ミュータブルな状態の参照渡しで累積値が扱えるようになっているようです。
ついでに、でやりたかったことはこれで解決してしまったうえ、参照渡しなので
コピーオンライトである配列やDictionaryを扱うときのパフォーマンスも申し分ないのですが……。

とはいえ作ってみたもの

なんたって主目的はミュータブル操作の排除なので、作ったものは載せときます。

オーバーロードとか用意したら少し長くなっちゃったので
中身のコードはオンラインエディタで
(リンク消える可能性を考慮して下部にも記載しておきます)

メインの関数だけ抜き出し
    func toDictionary<TElement,TKey,TValue>(
        _ keyFunc:   (TElement) throws -> TKey,
        _ valueFunc: (TElement) throws -> TValue,
        uniquingKeysWith: (TValue, TValue) throws -> TValue
    )rethrows -> Dictionary<TKey,TValue>
    where TElement == Element {
        let mappedItems = try self
            .lazy
            .map { (key: try keyFunc($0), value: try valueFunc($0)) }   // 1.
        return try Dictionary(mappedItems,
                              uniquingKeysWith: uniquingKeysWith)       // 2.
    }

やってることは

  1. mapping関数を引数にとって、任意の配列を(key:TKey, value:TValue)のシーケンスに変換
  2. Dictionary.init(_:uniquingKeysWith:)で任意のKeyVaklueタプルのシーケンスからDictionaryインスタンスを生成。
    (キー重複が起こりえるのでその場合の処理を明示化)
使い方

let keyValues =  [1:"a",
                  2:"b",
                  3:"c",
                  4:"a",]

let keyStrSet = keyValues.keys
    .toDictionary({$0},
                  {String($0)},
                  ToDictionaryUnique.adoptLast)
print(keyStrSet) // [1: "1", 2: "2", 3: "3", 4: "4"]


let keyValueSwapLast = keyValues
    .toDictionary({$0.value},
                  {$0.key},
                  ToDictionaryUnique.adoptLast) 
print(keyValueSwapLast) // ["c": 3, "a": 4, "b": 2]

let keyValueSwapFirst = keyValues
    .toDictionary({$0.value},
                  {$0.key},
                  ToDictionaryUnique.adoptFirst) 
print(keyValueSwapFirst) // ["c": 3, "a": 1, "b": 2]

let uniqueError = try keyValues
    .toDictionary({$0.value}, 
                  {$0.key})    // Fatal error: Error raised at top level: ToDictionaryError.duplicateKeys
print(uniqueError)
どこかで見たことあるような……?

C#やってたので、C#にあったIEnumerable.ToDictionary()の使い勝手をそのまま移植した感じ。
Swiftは例外発生時の動作を漏れなく扱えるのがいいですね。

個人的な発見とか思い
  • rethrowsthrow を含む関数を引数に取った時だけtryを必要とし、含まない場合はtry記述を必要としない。
  • 最初はthrowsする関数としない関数の2種類作る必要があるんじゃないかと思っていたので、関心した。
// 引数に取った関数で値を変換する
func myMap<T,TResult>(_ target:T, _ mapFunc: (T) throws -> TResult) rethrows -> TResult {
    return try mapFunc(target)
}

enum MyError:Error{case Err}
let unwrap = { (x:Int?) -> Int in 
                guard  let y = x else { throw MyError.Err }
                return y
             }
print(    myMap(Optional(1), {x in x!}) ) // 1
print(try myMap(Optional(1), unwrap )   ) // 1
print(try myMap(Int(""),     unwrap )   ) // Fatal error: Error raised at top level: MyError.Err: 
  • 結局のところ正解例の参照渡しと、今回作ったコードでどれくらいパフォーマンス(速度、メモリ使用量)に差が出るのか。
  • swiftのメモリ使用量ってどうやってみるんだろう。
  • そもそもXCodeは使ってなくて、WindowsのWSLで動かしてるからステップ実行とか、変数内参照とかすらまだ環境が出来てない…。

往々にしてよくある悲しみ
  • 動くものを作り、文章を書いてる段になって、書いてることの正しさ確認のために資料読んでて正解を見つける。
  • やってたことは一体なんだったの感。
  • まぁ理解した内容が無駄になるわけじゃないし、目的はmutableな要素を扱わないこととしたので、意味はあるのか…?







リンク切れのときのための中身

enum ToDictionaryError:Error{
    case duplicateKeys
}

/// キー重複時の標準的な操作を提供
enum ToDictionaryUnique {
    /// シーケンスで後に登場した要素を採用
    case adoptLast
    /// シーケンスで後に登場した要素を採用
    case adoptFirst

    func getUniqueKeyFunc<TValue>()
        -> (TValue,TValue)->TValue {
        switch self {
        case .adoptFirst:
            return {(old:TValue, new:TValue) in old }
        case .adoptLast:            
            return {(old:TValue, new:TValue) in new }
        }
    }
}


extension Sequence {

    /// SequenceからDictionaryに変換する。
    /// - Parameters:
    ///   - keyFunc:   DictionaryのKeyとなる値を返す関数
    ///   - valueFunc: DictionaryのValueとなる値を返す関数
    ///   - uniquingKeysWith: キー重複時採用する値を返す関数
    func toDictionary<TElement,TKey,TValue>(
        _ keyFunc:   (TElement) throws -> TKey,
        _ valueFunc: (TElement) throws -> TValue,
        uniquingKeysWith: (TValue, TValue) throws -> TValue
    )rethrows -> Dictionary<TKey,TValue>
    where TElement == Element {
        let mappedItems = try self
            .lazy
            .map { (key: try keyFunc($0), value: try valueFunc($0)) }
        return try Dictionary(mappedItems,
                              uniquingKeysWith: uniquingKeysWith)
    }

    /// SequenceからDictionaryに変換する。
    /// - Parameters:
    ///   - keyFunc:   DictionaryのKeyとなる値を返す関数
    ///   - valueFunc: DictionaryのValueとなる値を返す関数
    ///   - uniqueCase: キー重複時の動作
    func toDictionary<TElement,TKey,TValue>(
        _ keyFunc:   (TElement) throws -> TKey,
        _ valueFunc: (TElement) throws -> TValue,
        _ uniqueCase: ToDictionaryUnique
    ) rethrows -> Dictionary<TKey,TValue>
    where TElement == Element {
        return try toDictionary(keyFunc,
                                valueFunc,
                                uniquingKeysWith: uniqueCase.getUniqueKeyFunc())
    }

    /// SequenceからDictionaryに変換する。
    /// キー重複時は例外を発生する。
    func toDictionary<TElement,TKey,TValue>(
        _ keyFunc:   (TElement) throws -> TKey,
        _ valueFunc: (TElement) throws -> TValue
    ) throws -> Dictionary<TKey,TValue>
    where TElement == Element {        
        let throwDuplicateKey = { (_:TValue, _:TValue) -> TValue in
            throw ToDictionaryError.duplicateKeys 
        } 
        return try toDictionary(keyFunc,
                                valueFunc,
                                uniquingKeysWith: throwDuplicateKey)
    }
}

let keyValues =  [1:"a",
                  2:"b",
                  3:"c",
                  4:"a",]

let keyStrSet = keyValues.keys
    .toDictionary({$0},
                  {String($0)},
                  ToDictionaryUnique.adoptLast)
print("keyStrSet")
print(keyStrSet) // [1: "1", 2: "2", 3: "3", 4: "4"]


let keyValueSwapLast = keyValues
    .toDictionary({$0.value},
                  {$0.key},
                  ToDictionaryUnique.adoptLast) 
print("keyValueSwapLast")
print(keyValueSwapLast) // ["c": 3, "a": 4, "b": 2]

let keyValueSwapFirst = keyValues
    .toDictionary({$0.value},
                  {$0.key},
                  ToDictionaryUnique.adoptFirst) 
print("keyValueSwapFirst")
print(keyValueSwapFirst) // ["c": 3, "a": 1, "b": 2]

//let uniqueError = try keyValues
//    .toDictionary({$0.value}, 
//                  {$0.key})    // Fatal error: Error raised at top level: ToDictionaryError.duplicateKeys
//print(uniqueError)
0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?