右上➚

プログラミングに関するメモをのこしていきます

BK-tree を golang で実装した

先日はてぶに 興味深いデータ構造:BK木 | プログラミング | POSTD という翻訳記事 ( 元記事 http://signal-to-noise.xyz/post/bk-tree/) があがっているのをみて初めて BK-tree というものを知ったので,golang で実装してみました.

github.com

BK-tree とは

先程の記事に全部書いてあるのですが… BK-tree は,ある距離空間内で近傍点探索を効率的に行えるデータ構造です.利用例としてはスペルチェックや類似画像検索などがあります.

距離空間とは,なにかしらの距離を計算することができる空間のことで,距離としてハミング距離やマンハッタン距離,レーベンシュタイン距離,ユークリッド距離などなどが挙げられます. 例えば,いわゆる普通の 3 次元の空間は,ユークリッド距離を距離関数に持つ距離空間と考えられます.

近傍点探索は,要するにある点に対して,近くにある点を探すことです.

f:id:agtn:20170513183241p:plain

ものすごく単純に近傍点探索をやろうと思うと,全要素を線形に探索して距離を計算していく必要があります. そこで BK-tree を使うともっと計算量が減らせるというわけなのです.(ちなみに僕は BK-tree を使った場合の計算量がよくわかっていません.実験的に速いことを確認しただけです.正確な計算量は考えてもよくわかりませんでした…)

構造や仕組みは元記事などをご参照ください.

golang での bk-tree 実装は実はいくつかあったのですが,単純にアルゴリズムを理解するために実装したかったのと,レーベンシュタイン距離を距離関数として使うことを前提にしていたりとちょっと自分の用途に合っていない気がしたので,別で作ってみました.

このパッケージでは,スペルチェックそのものや距離関数を提供していません.BK-tree というデータ構造だけを提供しています.

使用例

16 bit 整数を要素,ハミング距離を距離関数とした例です.

要素となる型は bktree.Entry interface を満たす必要があります.これは Distance(Entry) int を要求する interface です.
本当は distance<T: Entry>(x: T) -> int みたいな形にしたいのですが,golang だと出来ないので,実装側で e.(hashValue) のように型アサーションする必要があります.
ここでは uint16 に戻してハミング距離を計算しています.

要素の追加は Add(e Entry) です.
ここでは 0 ~ 0xffff までを突っ込んでいます.

探索は Search(e Entry, tolerance int) []*Result です. 第一引数がキー,第二引数が許容する距離の最大値です.

package main

import (
    "fmt"

    "github.com/agatan/bktree"
)

type hashValue uint16

// Distance calculates hamming distance.
func (h hashValue) Distance(e bktree.Entry) int {
    a := uint16(h)
    b := uint16(e.(hashValue))

    d := 0
    var k uint16 = 1
    for i := 0; i < 16; i++ {
        if a&k != b&k {
            d++
        }
        k <<= 1
    }
    return d
}

func main() {
    var tree bktree.BKTree

    // add 0x0000 to 0xffff to the tree.
    for i := 0; i < 0xffff; i++ {
        tree.Add(hashValue(i))
    }

    // search neighbors of 0x0000 whose distances are less than or equal to 1.
    results := tree.Search(hashValue(0), 1)
    for _, result := range results {
        fmt.Printf("%016b (distance: %d)\n", result.Entry.(hashValue), result.Distance)
    }
}

これを実行すると,

0000000000000000 (distance: 0)
0000000000000001 (distance: 1)
0000000000000010 (distance: 1)
0000000000000100 (distance: 1)
0000000000001000 (distance: 1)
0000000000010000 (distance: 1)
0000000000100000 (distance: 1)
0000000001000000 (distance: 1)
0000000010000000 (distance: 1)
0000000100000000 (distance: 1)
0000001000000000 (distance: 1)
0000010000000000 (distance: 1)
0000100000000000 (distance: 1)
0001000000000000 (distance: 1)
0010000000000000 (distance: 1)
0100000000000000 (distance: 1)
1000000000000000 (distance: 1)

という感じで 0x0000 とのハミング距離が 0 ~ 1 である要素がとれます.

パフォーマンス

単純な線形探索との比較をするベンチマークをおいてあります.
64 bit 整数,距離関数はハミング距離,データ量 1,000,000 件での結果が以下のようになりました.

ベンチマーク 実行時間
BK-tree (完全一致) 1108 ns/op
BK-tree (fuzziness 1) 29468 ns/op
BK-tree (fuzziness 2) 328753 ns/op
BK-tree (fuzziness 4) 5490888 ns/op
BK-tree (fuzziness 8) 68182122 ns/op
BK-tree (fuzziness 32) 353715305 ns/op
Linear Search 4132926 ns/op

fuzziness が小さければ小さいほど ( = tolerance が小さければ小さいほど ) 高速に探索できることが分かります.

また,データ量が増えるほど Linear Search より有利になるので,距離に対してデータが十分に大量にある場合はかなり有効といえそうです.

おまけ

tree の構築にかかるコストがそこそこ大きかったので pprof で見つつチューニングする必要がありました. 学びとして,「map が重い」「interface が重い」というのがありました.

各ノードの部分木は,そのノードからの距離 d を key として,map[int]*Node としていました. tree を構築する際には,allocate + read + write をかなりの回数行うのですが,これがまぁ遅い.
最終的にこの部分はスライスでもっておいて,d を key として部分木をとりたい時は線形探索をするようにしました.
next, ok := n.children[d] としていた部分が

type elem struct
    distance int
    node *node
}

for _, c := range n.children {
    if c.distance == d {
        return c.node
    }
}
return nil

という感じになります.あんまりきれいではないんですが,こっちの方がほとんどのケース倍以上速かったので,こちらを採用しました.
部分木の数が増えてくると,map のほうが速いと思われるのですが,ハミング距離の場合最大でも bit 数までしか部分木が増えないので.
レーベンシュタイン距離を用いたスペルチェックの場合でも,単語の最大文字数以上の距離にはなりません. 2 次元 / 3 次元程度の距離空間なら kd-tree などもっと他に良い方法があるきがするので,レーベンシュタイン距離やハミング距離を使うケースをメインに考えました.

その結果,実行時間のかなりの割合が Entry interface を介した関数呼び出しのオーバヘッドとか,inteface の allocation になってしまいました.
データ構造を golang で提供する以上このオーバヘッドは避けられないです.( もちろん BK-tree そのものを,自分の利用形態に特化して作れば回避できますが… )
ちょっと Rust や C++ で書きたくなりました.十分速いし書きやすいので良いんですが…

Rust でグラフ構造や木構造を作る

プログラムを書いていると何かしら木構造っぽいものやグラフっぽいものを作りたい場面が多々あると思います. Rust は所有権や Size の都合で,これらを作ろうと思うと地味にハマるのでまとめておきます.

Rust で木構造

最も単純な木構造は Rust だと

enum Tree<T> {
    Leaf(T),
    Node(Box<Tree<T>>, Box<Tree<T>>),
}

といった形で表せます. Rust では明示的に boxing してあげないと再帰的なデータ構造は作れないのでちょっと複雑に見えるかもしれませんが,まぁ単純です.

この木構造を書き換えたい場合は,ownership をとって書き換えた値を返すこともできますし,&mut Tree<T> をとって in-place に書き換えることもできます.

fn inc(&mut self) {
    match *self {
        Tree::Leaf(ref mut i) => *i = *i + 1,
        Tree::Node(ref mut left, ref mut right) => {
            left.inc();
            right.inc();
        }
    }
}

fn inc2(self) -> Tree<i32> {
    match self {
        Tree::Leaf(i) => Tree::Leaf(i + 1),
        Tree::Node(left, right) => Tree::Node(Box::new(left.inc2()), Box::new(right.inc2())),
    }
}

Rust で有向非巡回グラフ

有向非巡回グラフ構造は,グラフのエッジに向きがあり,かつ循環がない構造で,これは割りと単純に表現できます.

struct Node<T>(T, Vec<Rc<Node<T>>>);

Node は自身の値 T と,つながっているノードの Vec を持ちます.(対象問題によっては Vec ではなくて HashMap とか HashSet とか)

Rc は参照カウント方式のスマートポインタです. グラフでは,Node は複数の Node から参照される可能性があるので, Box は使えません.

これを変更可能にしたい場合はちょっと面倒ですが RefCell を使う必要があります.

Rust では基本的に mutable borrow は常にひとつしか存在できず,mutable borrow が生きている間は immutable borrow もつくることができません. Rc から mutable な参照を取り出すこともできません.

そこで RefCell を使うことで borrow check をランタイムに行うようにします. RefCell は immutable な参照から mutable な参照を取り出せるようにする働きをしますが, mutable な参照を取っている間に,さらに mutable な参照を作ろうとしたり immutable な参照を作ろうとすると,ランタイムに panic します. コンパイル時検査ではなくランタイム検査になるので,プログラマの責任できちんと管理しないと死にます.

RefCell 版がこちら

struct Node<T>(T, Vec<Rc<RefCell<Node<T>>>>);

impl Node<i32> {
    fn inc(&mut self) {
        self.0 += 1;
        for n in &self.1 {
            n.borrow_mut().inc();
        }
    }
}

Rust で巡回有向グラフ

循環がある場合は厄介です. Rust で参照を共有するための Rc は参照カウントなので,循環参照があるとリークします. したがって循環のある構造を表すために Rc は使えません.

木構造で親を参照するポインタを子に持たせておきたいといったケースでは,親は子を Rc で持ち,子は親を Weak で持つという形で対応できますが, グラフだとそういうわけにもいきません.

そこで出て来るのが Arena という方法です.

オブジェクトの実体は Arena の中に作り,グラフにはその ID を持たせて管理します.

type NodeId = usize;

struct NodeArena<T> {
    arena: Vec<Node<T>>,
}

impl<T> NodeArena {
    fn alloc(&mut self, value: T) -> NodeId {
        let id = self.arena.len();
        let node = Node(id, value, Vec::new());
        self.arena.push(node);
        id
    }
    fn get(&self, id: NodeId) -> &Node<T> {
        &self.arena[id]
    }
    fn get_mut(&mut self, id: NodeId) -> &mut Node<T> {
        &mut self.arena[id]
    }
}

struct Node<T>(NodeId, T, Vec<NodeId>);

こんな感じです.

こうすることでコンパイル時の borrow check を諦めることなくグラフ構造を作ることができます.
(ii+1 番目のノードを同時に mutable に参照したいとかは苦しいですが)

欠点としては単純に間接的な表現でめんどくさいというのもありますが,GC がないので参照されなくなったオブジェクトも Arena 上で生き続けてしまうことです. そのため,動的に要素が生きたり死んだりするケースには使いにくいです.

ノードグラフの構築が終わったら Arena をリフレッシュするみたいなことをすると良いのかもしれません. (構築中は VecArena を表現して,構造が固まったら HashMap を使った Arena に切り替えて参照されているidだけを残すみたいな)
それでも構築中はオブジェクトがあまりまくるので辛いですね...

mio で echo サーバメモ

Rust の非同期 IO ライブラリのなかでももっとも低レベルなレイヤーを担っている mio を使ってecho サーバを書いた。 echo サーバばっかり書いているような気がするけど,echo サーバやっておくと簡単な割にライブラリの使い方とかがちゃんと分かる気がするので好きです。

コード

extern crate mio;

use std::io::prelude::*;
use std::collections::HashMap;

use mio::*;
use mio::tcp::{TcpListener, TcpStream};

#[derive(Debug)]
struct ClientsHolder {
    table: HashMap<Token, TcpStream>,
    free_token: Vec<Token>,
    next_max_token: Token,
}

impl ClientsHolder {
    fn new_from(start_token: Token) -> Self {
        ClientsHolder {
            table: HashMap::new(),
            free_token: Vec::new(),
            next_max_token: start_token,
        }
    }

    fn next_token(&mut self) -> Token {
        if let Some(tok) = self.free_token.pop() {
            return tok;
        }
        let tok = self.next_max_token;
        self.next_max_token = Token(tok.0 + 1);
        tok
    }

    fn register(&mut self, tok: Token, client: TcpStream) {
        self.table.insert(tok, client);
    }

    fn get_mut(&mut self, tok: Token) -> Option<&mut TcpStream> {
        self.table.get_mut(&tok)
    }

    fn remove(&mut self, tok: Token) -> Option<TcpStream> {
        let result = self.table.remove(&tok);
        if result.is_some() {
            self.free_token.push(tok);
        }
        result
    }
}

// Setup some tokens to allow us to identify which event is
// for which socket.
const SERVER: Token = Token(0);

fn main() {

    let addr = "127.0.0.1:13265".parse().unwrap();

    // Setup the server socket
    let server = TcpListener::bind(&addr).unwrap();

    // Create an poll instance
    let poll = Poll::new().unwrap();

    // Start listening for incoming connections
    poll.register(&server, SERVER, Ready::readable(), PollOpt::edge())
        .unwrap();

    // Create storage for events
    let mut events = Events::with_capacity(1024);
    let mut clients = ClientsHolder::new_from(Token(1));

    loop {
        poll.poll(&mut events, None).unwrap();

        for event in events.iter() {
            match event.token() {
                SERVER => {
                    // Accept and drop the socket immediately, this will close
                    // the socket and notify the client of the EOF.
                    let (stream, _) = server.accept().unwrap();
                    let tok = clients.next_token();
                    poll.register(&stream, tok, Ready::readable(), PollOpt::edge()).unwrap();
                    clients.register(tok, stream);
                }
                tok => {
                    let mut close = false;
                    if let Some(mut stream) = clients.get_mut(tok) {
                        let mut buf = [0; 1024];
                        let n = stream.read(&mut buf).unwrap();
                        if n == 0 {
                            poll.deregister(stream).unwrap();
                            close = true;
                        } else {
                            stream.write(&buf[0..n]).unwrap();
                        }
                    }
                    if close {
                        clients.remove(tok);
                    }
                }
            }
        }
    }
}

面倒だったので unwrap まみれですが。

やったこと

mio の全体の流れとしては,Poll 型の値がイベントを監視する役割を担います. Poll に監視対象を登録していき,Poll::poll でイベントの発火を待ちます. 発火したイベント一覧が Events 型の Events::iter で取れるので,対応していけばよいです.

mio では Token という型の値を使って監視対象を識別しています. 監視対象には TcpListenerTcpStreamSender,などなどいろんなものがあるので,統一的に扱うために PollToken だけを保持します. Token と監視対象の紐付けはユーザ側の責任でやってくれという感じみたいです.

echo サーバではクライアントの数は不特定なので,「空いている Token を探す」と「Token に対応するクライアント (TcpStream) を探す」がうまくできる必要があります. そこで,ClientsHolder を定義しました. こいつが,空いている Token を返すのと Token をキーに TcpStream を返す仕事をします. remove されたらその Token は再利用します.

気になるところ

mio はファイルに対する抽象化は提供しない方針のようです. STDIN / STDOUT も同様です.

ファイル IO もノンブロッキングにしたい場合はどうしたらいいんでしょう?よくわかっていない. mio::unix 以下に UNIX 限定の拡張がおいてあって,EventedFd という file descriptor を扱う実装はあるので,UNIX 限定なら力技でなんとかなるのかもしれない.

あと mio は関係ないんですが,実装の部分で,

let mut close = false;
if let Some(mut stream) = clients.get_mut(tok) {
    let mut buf = [0; 1024];
    let n = stream.read(&mut buf).unwrap();
    if n == 0 {
        poll.deregister(stream).unwrap();
        close = true;
    } else {
        stream.write(&buf[0..n]).unwrap();
    }
}
if close {
    clients.remove(tok);
}

というのがあるんですが,これどうやったらスマートなんでしょう. close = true としている部分で clients.remove(tok); をやるのが普通だと思うんですが,if let Some(mut stream) = clients.get_mut(tok) { のところで clients は borrow されているから mutable borrow はこれ以上作れないのです.

NCurses の Crystal binding を作った

この記事は、 Crystal Advent Calendar 2016 の8日目の記事です。 qiita.com

ncurses という CUI を作るためにスクリーンやキー入力を扱う有名なライブラリの Crystal binding を作りました。

github.com

ほとんど C の ncurses と同じ感じで使えるようになっていると思います. RubyPythoncurses ライブラリを参考にしつつ,なるべく使用感がかわらないようにしています.

examples

require “ncurses”

NCurses.open do
  NCurses.cbreak
  NCurses.noecho
  NCurses.move(x: 30, y: 20)
  NCurses.addstr(“Hello, world!”)
  NCurses.refresh

  NCurses.notimeout(true)
  NCurses.getch
end

NCurses.addstr とか NCurses.move とかは ncurses で言う addstrmove に当たる関数で,stdscr に対して waddstr とか wmove するやつです.

w = NCurses::Window.new(height: height, width: width) とすることで subwindow が作れます. w.addstrw.move という形で w prefix な関数たちが呼べるようになっています.

padattron / attroff などなども使えます.

詳細な例は example/ 以下のにおいてあります

なぜ作ったのか

実は ncurses bindings for Crystal はすでにあります.(https://github.com/jreinert/ncurses-crystal)
curses が Crystal の標準ライブラリから外されることが決まったときの CHANGELOG を見ると,今後はそっちを使ってねと書いてあったりします.

じゃあなんでわざわざ別で作ったのかという話ですが,API が足りない & 提供する API の形を変えたいと思ったからです.

単純に bind されている API が少なくてやりたいことができなかったので,最初は追加して PR を出そうと思っていたのですが,すでに提供されている API が割と高級になっていて 1 : 1 で C API と対応していない感じでした. 個人的には C library の wrapper にはなるべく薄くなっていてもらって基本的な API は覚え直さないでも使えるようになっていてほしいというふうに思ったので,C API と 1 : 1 で対応した形の API を提供する wrapper を作ってみようという経緯で新しく作ることにしました.

おまけ

C bindings を書くときに,wrapper API として LibC::Int が登場しちゃうのがなんとなく嫌で,LibC::Int を C が要求してくる関数を呼ぶ wrapper 関数には型指定をあえてしないという選択をしたんですがどうなんでしょう.

lib LibNCurses
  fun wmove(w : Window, y : LibC::Int, x : LibC::Int) : Result
end
class Window
  def move(y, x)
     LibNCurses.wmove(@win, y, x)
  end
end

みたいな感じです.(多少簡略化しています)

これどうなんですかね.なるべく外に見せる API には型を明記するようにしたかったのですが,LibC:Int 系は環境によって異なるのでそういうわけにも行かず…

y : LibC::Int, x : LibC::Int とかは別に良いんですが,ncurses は文字と属性をくっつけた chtype なる型を持っていてこれが結構厄介というか混乱を招くのではと思っています. chtypechar ではなく unsigned int で,文字と属性を bitor でくっつけたものになっています.addch のように char をとることを連想させる関数の引数が実は chtype = unsigned int でしかも Crystal の文字型 Char は 32bit なのでものすごく混乱します…

C は型変換を勝手にやってくれるので,unsigned int を返す関数から受け取った値を short を受け取る関数に渡すみたいなことをよくやっていて,Crystal のような型変換を暗黙にやらない言語から使おうとすると難しいんだなぁと思いました. なにか良い方法があればぜひ知りたいです.

Rust で Box に包まれた構造体の所有権分解

ちょっとはまったのでメモ

struct A {
    foo: Vec<i32>,
    bar: Vec<bool>,
}

こんな構造体があったとする。 普通、A の所有権を分解して foobar にしたいときは

fn xxx(x: A) -> (Vec<i32>, Vec<bool>) {
    let A { foo, bar } = x;
    (foo, bar)
}

とやれば良い(この例だともっと簡単に書ける気もするけど)

一方、Box<A> から foobar に分解したい場合は話が変わってくる。

fn error1(x: Box<A>) -> (Vec<i32>, Vec<bool>) {
    (x.foo, x.bar)
}

fn error2(x: Box<A>) -> (Vec<i32>, Vec<bool>) {
    let A { foo, bar } = *x;
    (foo, bar)
}

これらは両方共コンパイルできない。 人間から見ると,Box<A>A の所有権を持っているのだから、A -> foo/bar に分解できるなら Box<A> も同様にできる気がする。

実際にはこのようにするとコンパイルが通る。

fn success(x: Box<A>) -> (Vec<i32>, Vec<bool>) {
    let x = *x;
    let A { foo, bar } = x;
    (foo, bar)
}

うーん、エラーになるケースだと Deref トレイトの機能を経由している感じになるのかな? Deref 経由で foo の所有権をとるとその時点で Box<A> の所有権は奪われちゃうから bar の所有権が取れないということなのだと想像した。 success のようなコードが突然出てきたら混乱しそうだ。

日本語の改行を適当にいい感じにするツールを作りました

必要に迫られて、HTML ページ内の改行位置をいい感じにするツールを作ってみました。

github.com

HTMLに長文を書くと、親 DOM のサイズの制約上、適宜改行がぶちこまれます。 しかし、改行位置は文節を考慮などせずにごりっと挿入されるので、多くの問題が生じることが報告されています。 最も有名な問題として、今すぐダウンロー
ド問題が挙げられます。

japawrap を使うと、それっぽく日本語を解釈して <span> でくくるみたいなことができます。inline-block を適用すれば改行がそれっぽく入るようにできます。

Install

go get github.com/agatan/japawrap/...

japawrap コマンドが使えるようになります。

Usage

CLI

ファイル名を指定するか標準入力から流し込みます。

$ echo "今日も元気です" | japawrap
<span class="wordwrap">今日も</span><span class="wordwrap">元気です</span>

このように適宜いい感じに wrap してくれます。

オプションとして -open string-close string をサポートしているので、

$ echo "今日も元気です" | japawrap -open '<span style="display: inline-block;">' -close "</span>" 
<span style="display: inline-block;">今日も</span><span style="display: inline-block;">元気です</span>

みたいなことができます。

Library

一応 japawrap はライブラリとしても使用できるようになっています。

w := japawrap.New(open, close)
s := "今日も元気です"
fmt.Println("%s => %s", s, w.Do(s))

これだけです。

Example

では実際に使った結果を下に記したいと思います。 文章は、上の方に自分で書いた文章をそのまま使います。


before

HTMLに長文を書くと、親DOM のサイズの制約上、適宜改行がぶちこまれます。 しかし、改行位置は文節を考慮などせずにごりっと挿入されるので、多くの問題が生じることが報告されています。 最も有名な問題として、今すぐダウンロード問題が挙げられます。

after

HTMLに長文を書くと、DOM のサイズの制約上、適宜改行がぶちこまれます。 しかし、改行位置は文節を考慮などせずにごりっと挿入されるので、多くの問題が生じることが報告されています。 最も有名な問題として、今すぐダウンロード問題が挙げられます。


こんな感じになります。猛烈に効果がわかりにくくて驚いていますが、一応効果はちゃんと出ているのではないでしょうか? HTMLを直接見ていただければどうなっているかはわかると思います。

次に青空文庫から夏目漱石「こころ」の序文を抜粋してみたのが下の画像たちです。

f:id:agtn:20161029131814p:plainf:id:agtn:20161029131817p:plainf:id:agtn:20161029131820p:plain

レスポンシブ!それなりになっている気がします。

あとがき

この問題へのアプローチとして、https://github.com/google/budou が有名だと思います。

budou は Cloud Natural Language API を内部で使っていて、しっかり日本語の文章を構文解析しているようです。 なので非常に精度は高いと思うのですが、今僕の GCP アカウントがごにょごにょしていてぱぱっと試せる状況ではなかったので自作しました。

budou と違ってしっかり構文解析などはしていなくて、形態素解析した後、それっぽく分割しているだけです。なので精度は落ちると思います。 一方、budouGCP の credentails が必要だったりと準備が必要になるので、お手軽に試せるというのは悪くないかなと思っています。

Rust の Result と Iterator

Rust には失敗するかもしれない値を表す Result<T, E> という型があります。 std::result::Result

そして iterate できることを表す Iterator という trait があります。 std::iter::Iterator

また、Iterator trait は要素型を表す関連型を持ちます。例えば StringIterator<Item=char>impl しています。これは char 型を要素にもつ Iterator であることを意味します。 ここ間違っていました。String が直接 Iteratorimpl しているのではありませんでした。

たまに Iterator<Item=Result<T, E>> のようになっている型を見かけます(T, E にはなにかしら具体的な型が入っていると思ってください)。 例えば、std::io::stdin().bytes() の返り値である std::io::BytesIterator<Item=Result<u8>>impl しています。 (ちょっとわかりにくいのですがここでの Resultstd::result::Result ではなくて std::io::Result です。std::io::Resultstd::result::Result<T, std::io::Error>エイリアスです。)

さて、このような Iterator からすべての要素が Ok(_) であれば Ok<Vec_>> を、Err(_) があれば Err<_> を返すような処理を書きたいということは割りとよくあります。 で、これを一生懸命実装しようとしていたのですが、標準ライブラリの範囲内ですでに実装されていました。べんり。

let result = std::io::stdin().bytes().collect::<Result<Vec<_>, _>>();

これだけです。これで要件を満たす Result<Vec<_>, _> が返って来ます。すばらしい。

タネは簡単な話で ResultFromIterator trait を impl しているので collect で変換が可能であるというお話でした。 std::iter::FromIterator