C++20:Python マップ関数

今日、私は愛する Python 関数を C++ で書く実験を終えました。これまでのところ、Python 関数の filter、range、および xrange を実装しました。今日は、map 関数を詳しく見て、関数 map と filter を 1 つの関数に結合します。

私の最後の投稿「C++20:Pythons range Function, the Second」で、range:xrange の遅延バリアントを実装しました。私のドイツの読者の何人かは、Python 2 の xrange 関数など、xrange が動作しないと不満を漏らしています。私の xrange 関数では、作成された数値の最初と最後に定数式が必要です。

auto res = xrange<1, 10>(); 
for (auto i: res) std::cout << i << " "; // 1 2 3 4 5 6 7 8 9

この例では、1 と 10 が定数式です。これは、次のような式はコンパイルされないことを意味します。

int begin = 1;
int end = 10;

auto res = xrange<begin, end>(); 

それが何を意味するか知っていると思いますか?

Python 範囲関数、第 3

ドイツの読者である Clocktown のおかげで、本日、xrange の最終バージョンを発表できます。関数 xrange はレイジーであり、定数式ではない境界の引数も受け入れることができます。

// xrange2.hpp

#include <stdio.h> #include <range/v3/all.hpp> namespace view = ranges::views; auto xrange(long long begin, long long end, long long step = 1) { if(step == 0) { throw std::invalid_argument("Step cannot be 0"); } auto Step = begin < end ? step : -step; auto Begin = std::min(begin, end); auto End = Step < 0 ? Begin : std::max(begin, end); return view::iota(Begin, End) | view::stride(std::abs(Step)) | view::transform([begin, end](std::size_t i){ return begin < end ? i : end - (i - begin); }); } auto xrange(long long end) { return xrange(0, end, 1); }

彼の実装の重要なアイデアは、view::transform が最終的に計算を逆バリアントに変換することです。 xrange は、1 つ、2 つ、または 3 つの引数で呼び出すことができます。最初の引数のデフォルトは 0 で、3 番目の引数のデフォルトは 1 です。試してみましょう。前回の投稿の xrange 実装をこの新しい実装に置き換えました。

// range2.cpp

#include "xrange2.hpp"

#include <iostream>
#include <range/v3/all.hpp>
#include <vector>

 
int main() {
 
 std::cout << std::endl;

 auto res = xrange(1, 10);
 for (auto i: res) std::cout << i << " ";
 
 std::cout << "\n\n";
 
 res = xrange(1, 50, 5);
 for (auto i: res) std::cout << i << " ";
 
 std::cout << "\n\n";
 
 auto res2 = xrange(20, 10, -1);
 for (auto i: res2) std::cout << i << " ";
 
 std::cout << "\n\n";
 
 res2 = xrange(50, 10, -5);
 for (auto i: res2) std::cout << i << " ";
 
 std::cout << "\n\n";
 
 res = xrange(1, 1'000'000'000'000'000'000);
 // for (auto i: res) std::cout << i << " ";
 
 for (auto i: res | ranges::views::take(10)) std::cout << i << " ";
 
 std::cout << "\n\n";
 
 for (auto i: res | ranges::views::drop_while([](int i){ return i < 1'000'000; })
 | ranges::views::take_while([](int i){ return i < 1'000'010; })){
 std::cout << i << " ";
 }
 
 std::cout << "\n\n";
 
}

予想通り、同じ結果が得られます。

これまでのところ、新しいものはありません。しかし、ここに新しいユースケースがあります。 begin と end は定数式ではなく、xrange は 1 つの引数をサポートします。

int main() {

 int begin = 3;
 int end = 7;

 for(auto x: xrange(end)) {
 std::cout << x << " "; // 0 1 2 3 4 5 6
 }

 for(auto x: xrange(begin, end)) {
 std::cout << x << " "; // 3 4 5 6

 for(auto x: xrange(end, begin, -2)) {
 std::cout << x << " "; // 7 5
 }
 
}

これで、関数 range と xrange は完了です。関数マップを続けましょう。

地図

まず、Python 2 マップ関数の簡略化した定義を次に示します。マップを 1 つのシーケンスに制限します

  • map(function, sequence):入力シーケンスの各要素に関数を適用してリストを返します。

考えてみれば、克服すべき課題が 1 つあります。 Python 関数フィルター (C++20:Ranges ライブラリを使用した Pythonic) とは対照的に、map は入力シーケンスの型を変更できます。

// map.cpp

#include "range.hpp"

#include <algorithm>
#include <fstream>
#include <functional>
#include <iostream>
#include <range/v3/all.hpp>
#include <string>
#include <vector>
#include <utility>


template <typename Func, typename Seq>
auto map(Func func, Seq seq) {
 
 typedef typename Seq::value_type value_type;
 using return_type = decltype(func(std::declval<value_type>())); // (4)

 std::vector<return_type> result{};
 for (auto i :seq | ranges::views::transform(func)) result.push_back(i);
 
 return result;
}

int main() {
 
 std::cout << std::endl;
 
 // map(lambda i: i * i, range(1, 10)) // (1)
 auto res = map([](int i){ return i * i; }, range(1, 10) ); 
 
 for (auto v: res) std::cout << v << " ";
 
 std::cout << "\n\n";
 // (2)
 // map(lambda word: (len(word), word), ["Only", "for", "testing", "purpose."])
 std::vector<std::string> myStrings{"Only", "for", "testing", "purpose"};
 auto res2 = map([](const std::string& s){ return std::make_pair(s.size(), s); }, myStrings);
 
 for (auto p: res2) std::cout << "(" << p.first << ", " << p.second << ") " ;
 
 std::cout << "\n\n";
 // (3)
 // freqWord = map(lambda word: (len(word), word), open("/etc/services").read().split("\n"))
 // freqWord.sort(reverse = True)
 // freqWord[:3] 
 std::ifstream file("/etc/services", std::ios::in);
 std::stringstream buffer;
 buffer << file.rdbuf();
 std::string text = buffer.str();

 std::vector<std::string> words = text | ranges::views::split('\n'); // (4)
 auto lengthWords = map([](const std::string& s){ return std::make_pair(s.size(), s); }, words);
 std::sort(std::begin(lengthWords), std::end(lengthWords), std::greater);
 std::for_each(std::begin(lengthWords), std::begin(lengthWords) + 3, [](const auto& p) {
 std::cout << p.first << " " << p.second << std::endl;
 });
 
 std::cout << std::endl;
 
}

行 (4) は、return_type を推測します。 return_type は、関数 func が適用された場合に、入力シーケンスのすべての要素が変換される型です。 std::declval() は、型を推測するために decltype で使用できる右辺値参照を返します。

コメントアウトされた行は、対応する Python コードです。

<オール>
  • 各要素をその正方形にマッピングします
  • 各単語を単語と単語の長さのペアにマッピングします
  • ファイル「/etc/services」から各行を読み取り、各行を行と行の長さのペアにマッピングし、結果のシーケンスを逆順に並べ替え、最も長い 3 行を表示します。
  • スクリーンショットは、プログラムの出力を示しています。

    map 関数を実装しなければならなかった 1 つの追加の問題について言及するのをほとんど忘れていました。呼び出し std::vector words =text |範囲::ビュー::スプリット('\n'); (4 行目) は非推奨です。代わりに、変換演算子 range::to を使用する必要があります。 range::to は C++20 の一部ではないため、ranges ライブラリの作成者である Eric Niebler にどうすればよいか尋ねました。彼は、GCC のバグを引き起こす非常に冗長な解決策を提案しました。これは、Eric からのバグ レポート 93936 です。最後に、廃止されたバージョンに固執します。

    関数マップは私の実験の終わりではありません。私は自分自身に言いました。 map と filter を 1 つの関数に結合して、C++ のリスト内包表記に似たものを作成しましょう。正直なところ、この結果に 100% 満足しているわけではありません。

    リスト内包表記の一種

    Python のリスト内包表記とは対照的に、私の関数 mapFilter は 1 つのシーケンスしか処理できません。

    // mapFilter.cpp
    
    #include "range.hpp"
    
    #include <algorithm>
    #include <cctype>
    #include <fstream>
    #include <functional>
    #include <iostream>
    #include <range/v3/all.hpp>
    #include <string>
    #include <vector>
    #include <utility>
    
    template <typename T>
    struct AlwaysTrue { // (1)
     constexpr bool operator()(const T&) const {
     return true;
     }
    };
     // (2)
    template <typename Map, typename Seq, typename Filt = AlwaysTrue<typename Seq::value_type>>
    auto mapFilter(Map map, Seq seq, Filt filt = Filt()) {
     
     typedef typename Seq::value_type value_type;
     using return_type = decltype(map(std::declval<value_type>())); 
    
     std::vector<return_type> result{};
     for (auto i :seq | ranges::views::filter(filt) 
     | ranges::views::transform(map)) result.push_back(i);
     return result;
    }
    
    int main() {
     
     std::cout << std::endl; 
     // (3)
     // [ i * i for i in range(1, 10) ] 
     auto res = mapFilter([](int i){ return i * i; }, range(1, 10) );
     
     // (4)
     // [ i * i for i in range(1, 10) if i % 2 == 1 ]
     res = mapFilter([](int i){ return i * i; }, range(1, 10) , 
     [](auto i){ return i % 2 == 1; });
     
     for (auto v: res) std::cout << v << " ";
     
     std::cout << "\n\n";
     
     // (3) 
     // [(len(word), word) for word in ["Only", "for", "testing", "purpose."]]
     std::vector<std::string> myStrings{"Only", "for", "testing", "purpose"};
     auto res2 = mapFilter([](const std::string& s){ return std::make_pair(s.size(), s); }, myStrings);
     
     // (5)
     // [(len(word), word) for word in ["Only", "for", "testing", "purpose."] if word[0].isupper()]
     myStrings = {"Only", "for", "testing", "purpose"};
     res2 = mapFilter([](const std::string& s){ return std::make_pair(s.size(), s); }, myStrings, 
     [](const std::string& word){ return std::isupper(word[0]); });
     
     for (auto p: res2) std::cout << "(" << p.first << ", " << p.second << ") " ;
     
     std::cout << "\n\n";
     
     // (3) 
     // freqWord = [(len(line), line) for line in open("/etc/services").read().split("\n")]
     // freqWord = map(lambda word: (len(word), word), open("/etc/services").read().split("\n"))
     // freqWord.sort(reverse = True)
     // freqWord[:3] 
     std::ifstream file("/etc/services", std::ios::in);
     std::stringstream buffer;
     buffer << file.rdbuf();
     std::string text = buffer.str();
    
     std::vector<std::string> words = text | ranges::views::split('\n');
     auto lengthWords = mapFilter([](const std::string& s){ return std::make_pair(s.size(), s); }, words);
     std::sort(std::begin(lengthWords), std::end(lengthWords), std::greater());
     
     // (6)
     // len([line for line in open("/etc/services").read().split("\n") if 100 < len(line) < 150])
     words = text | ranges::views::split('\n');
     auto allLines = mapFilter([](const std::string& line){ return line; }, words, 
     [](const std::string& line){ return 100 < line.size() && line.size() < 150; });
     std::cout << "Number of lines: " << allLines.size();
     
     std::cout << "\n\n";
    }
    

    フィルター関数が適用する既定の述語 (2 行目) は、常に true を返します (1 行目)。常に true は、map 関数など、関数 mapFilter がデフォルトで動作することを意味します。番号 (3) が付けられたすべての行を調べると、前のプログラム map.cpp との違いはわかりません。しかし今、違いが始まります。 Python の対応するリスト内包表記はコメント アウトされています。

    • 行 (4) は、奇数である数値の 2 乗を計算します。
    • 行 (5) は、単語が大文字で始まる場合、ペア (単語の長さ、単語) を返します。
    • 行 (6) は、ファイル「/etc/services」のすべての行のベクトルを返します。これは 100 ~ 150 文字です。

    次は?

    今回の投稿はいつもより少し長めでした。次の投稿は、一時停止と再開が可能な一般化された機能についてです。簡単に言うと、次の投稿はコルーチンについてです。