Open15

[実験]PyO3を使ってPythonにRustの型を導入してみる

Yos_KYos_K

Option型を作ってみる
構造体を用意して、Option型をラップするだけ

#[pyclass(name="Option")]
#[derive(Debug, FromPyObject)]
pub struct RsOption {
    pub value: Option<PyObject>
}

#[pymethods]
impl RsOption {
    #[new]
    pub fn new(n: Option<PyObject>) -> Self {
        Self { value: n }
    }

    #[getter]
    fn value(&self) -> PyResult<Option<PyObject>> {
        Ok(self.value.clone())
    }
}
Yos_KYos_K

Option型のメソッドを実装する
基本的に内部でOption型の同名のメソッドを呼び出すだけだが、Pythonから関数を受け取って適用する場合は工夫がいる(たぶん)

    pub const fn is_some(&self) -> bool {
        self.value.is_some()
    }

    pub fn is_some_and(&self, f: PyObject) -> bool {
        match &self.value {
            None => false,
            Some(x) => {
                Python::with_gil(|py| {
                    f.call1(py, (x,)).map(|r| r.to_object(py)).unwrap()
                    .downcast_bound::<PyBool>(py).unwrap().extract().unwrap()
                })
            },
        }
    }
Yos_KYos_K

Python側でprintできるように__str__を実装する
もう少しいい書き方がありそうな気がする

    fn __str__(&self) -> String {
        let s = Python::with_gil(|py| {
            self.value().unwrap().to_object(py).to_string()
        });
        if &s != "None" {
            format!("Some({})", &s)
        } else {
            s
        }
    }
Yos_KYos_K

Python側ではスタブファイルをこんなかんじで用意しておく

from typing import Callable, Tuple, TypeVar, Generic


T = TypeVar('T')
U = TypeVar('U')
E = TypeVar('E')


class Option(Generic[T]):
    def __init__(obj: T) -> None: ...
    @property
    def value(self) -> T: ...
    def is_some(self) -> bool: ...
    def is_some_and(self, f: Callable[[T], bool]) -> bool: ...
    def is_none(self) -> bool: ...
    def expect(self, msg: str) -> T: ...
    def unwrap(self) -> T: ...
    def unwrap_or(self, default: T) -> T: ...
    def unwrap_or_else(self, f: Callable[[], T]) -> T: ...
    def map(self, f: Callable[[T], U]) -> Option[U]: ...
    def inspect(self, f: Callable[[T],]) -> Option: ...
    def map_or(self, default: U, f: Callable[[T], U]) -> U: ...
    def map_or_else(self, default: Callable[[], U], f: Callable[[T], U]) -> U: ...
    def ok_or(self, err: E) -> Result[T, E]: ...
    def ok_or_else(self, err: Callable[[], E]) -> Result[T, E]: ...
    def and_then(self, f: Callable[[T], Option[U]]) -> Option[U]: ...
    def or_else(self, f: Callable[[], Option[T]]) -> Option[T]: ...
    def zip(self, other: Option[U]) -> Option[Tuple[T, U]]: ...
Yos_KYos_K

Resultも同様に実装してみる

#[pyclass(name="Result")]
#[derive(Debug, FromPyObject)]
pub enum RsResult {
    Ok {value: PyObject},
    Err {value: PyObject}
}

#[pymethods]
impl RsResult {
    fn __str__(&self) -> String {
        match &self {
            RsResult::Ok { value } => format!("Ok({})", &value),
            RsResult::Err { value } => format!("Err({})", &value),
        }
    }
}
Yos_KYos_K

stubは冗長になるけどこんなかんじに書くと良さそう

class Result:
    class Ok(Generic[T]):
        def __init__(self, value: T) -> None: ...
        def is_ok(self) -> bool: ...
        def is_ok_and(self, f: Callable[[T], bool]) -> bool: ...
        def is_err(self) -> bool: ...
        def is_err_and(self, f: Callable[[T], bool]) -> bool: ...
        def ok(self) -> Option[T]: ...
        def err(self) -> Option[E]: ...
        def map(self, f: Callable[[T], U]) -> Result.Ok[U]: ...
        def map_or(self, default: U, f: Callable[[T], U]) -> U: ...
        def map_or_else(self, default: Callable[[], U], f: Callable[[T], U]) -> U: ...

    class Err(Generic[T]):
        def __init__(self, value: T) -> None: ...
        def is_ok(self) -> bool: ...
        def is_ok_and(self, f: Callable[[T], bool]) -> bool: ...
        def is_err(self) -> bool: ...
        def is_err_and(self, f: Callable[[T], bool]) -> bool: ...
        def ok(self) -> Option[T]: ...
        def err(self) -> Option[E]: ...
        def map(self, f: Callable[[T], U]) -> Result.Err[U]: ...
        def map_or(self, default: U, f: Callable[[T], U]) -> U: ...
        def map_or_else(self, default: Callable[[], U], f: Callable[[T], U]) -> U: ...
        def map_err(self, op: Callable[[T], U]) -> Result[U]: ...
Yos_KYos_K

stubがごちゃついてきたので整理
https://www.maturin.rs/project_layout.html?highlight=stub#adding-python-type-information
↑を参考に

Yos_KYos_K

python/rstypesにおいたstubをimportできるようにpyproject.tomlに以下の記載を追加

packages = [
    {include = "rstypes", from = "python"}
]
Yos_KYos_K

型を推論できるようにpython/rstypes/__ init __.pyに以下の内容を記述

from option import Option
from result import Result
Yos_KYos_K

実装したOptionのメソッド

class Option(Generic[T]):
    def __init__(self, obj: T) -> None: ...
    @property
    def value(self) -> T: ...
    def is_some(self) -> bool: ...
    def is_some_and(self, f: Callable[[T], bool]) -> bool: ...
    def is_none(self) -> bool: ...
    def expect(self, msg: str) -> T: ...
    def unwrap(self) -> T: ...
    def unwrap_or(self, default: T) -> T: ...
    def unwrap_or_else(self, f: Callable[[], T]) -> T: ...
    def map(self, f: Callable[[T], U]) -> Option[U]: ...
    def inspect(self, f: Callable[[T],]) -> Option: ...
    def map_or(self, default: U, f: Callable[[T], U]) -> U: ...
    def map_or_else(self, default: Callable[[], U], f: Callable[[T], U]) -> U: ...
    def ok_or(self, err: E) -> Result: ...
    def ok_or_else(self, err: Callable[[], E]) -> Result: ...
    def and_then(self, f: Callable[[T], Option[U]]) -> Option[U]: ...
    def or_else(self, f: Callable[[], Option[T]]) -> Option[T]: ...
    def zip(self, other: Option[U]) -> Option[Tuple[T, U]]: ...
    def transpose(self) -> Result[Option[T]]: ...
    def flatten(self) -> Option[T]: ...
Yos_KYos_K

Resultのメソッド

class Result:
    class Ok(Generic[T]):
        def __init__(self, value: T) -> None: ...
        def is_ok(self) -> bool: ...
        def is_ok_and(self, f: Callable[[T], bool]) -> bool: ...
        def is_err(self) -> bool: ...
        def is_err_and(self, f: Callable[[T], bool]) -> bool: ...
        def ok(self) -> Option[T]: ...
        def err(self) -> Option[E]: ...
        def map(self, f: Callable[[T], U]) -> Result.Ok[U]: ...
        def map_or(self, default: U, f: Callable[[T], U]) -> U: ...
        def map_or_else(self, default: Callable[[], U], f: Callable[[T], U]) -> U: ...
        def inspect(self, f: Callable[[T],]) -> Result: ...
        def inspect_err(self, f: Callable[[T],]) -> Result: ...
        def expect(self, msg: str) -> T: ...
        def unwrap(self) -> T: ...
        def expect_err(self, msg: str) -> T: ...
        def unwrap_err(self) -> T: ...
        def and_then(self, op: Callable[[T], Result]) -> Result: ...
        def or_else(self, op: Callable[[T], Result]) -> Result: ...
        def unwrap_or(self, default: T) -> T: ...
        def unwrap_or_else(self, op: Callable[[E], T]) -> T: ...
        def transpose(self) -> Option[Result.Ok[T]]: ...
        def flatten(self) -> Result.Ok[T]: ...

    class Err(Generic[T]):
        def __init__(self, value: T) -> None: ...
        def is_ok(self) -> bool: ...
        def is_ok_and(self, f: Callable[[T], bool]) -> bool: ...
        def is_err(self) -> bool: ...
        def is_err_and(self, f: Callable[[T], bool]) -> bool: ...
        def ok(self) -> Option[T]: ...
        def err(self) -> Option[E]: ...
        def map(self, f: Callable[[T], U]) -> Result.Err[U]: ...
        def map_or(self, default: U, f: Callable[[T], U]) -> U: ...
        def map_or_else(self, default: Callable[[], U], f: Callable[[T], U]) -> U: ...
        def map_err(self, op: Callable[[T], U]) -> Result: ...
        def inspect(self, f: Callable[[T],]) -> Result: ...
        def inspect_err(self, f: Callable[[T],]) -> Result: ...
        def expect(self, msg: str) -> T: ...
        def unwrap(self) -> T: ...
        def expect_err(self, msg: str) -> T: ...
        def unwrap_err(self) -> T: ...
        def and_then(self, op: Callable[[T], Result]) -> Result: ...
        def or_else(self, op: Callable[[T], Result]) -> Result: ...
        def unwrap_or(self, default: T) -> T: ...
        def unwrap_or_else(self, op: Callable[[E], T]) -> T: ...
        def transpose(self) -> Option[Result.Err[T]]: ...
        def flatten(self) -> Result.Err[T]: ...