Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/fable-library-py/fable_library/core/option.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ from typing import overload
from . import Array

class SomeWrapper[T]:
value: T
@property
def value(self) -> T: ...
def __init__(self, value: T) -> None: ...
def __repr__(self) -> str: ...
def __str__(self) -> str: ...
Expand Down
17 changes: 15 additions & 2 deletions src/fable-library-py/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,12 @@ impl SomeWrapper {
op: CompareOp,
py: Python<'py>,
) -> PyResult<bool> {
if other.is_none() {
return Ok(matches!(op, CompareOp::Ne | CompareOp::Gt | CompareOp::Ge));
}

let other_value = extract_value(py, other)?;
let result = self.value.bind(py).rich_compare(other_value, op)?;
result.is_truthy()
compare_values(self.value.bind(py), other_value.bind(py), op)
}

pub fn __repr__(&self, py: Python<'_>) -> PyResult<String> {
Expand Down Expand Up @@ -167,6 +170,16 @@ impl SomeWrapper {
}
}

// A `None` argument here is the inner option's `None` case, not an absent value.
fn compare_values(a: &Bound<'_, PyAny>, b: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
match (a.is_none(), b.is_none()) {
(true, true) => Ok(matches!(op, CompareOp::Eq | CompareOp::Le | CompareOp::Ge)),
(true, false) => Ok(matches!(op, CompareOp::Ne | CompareOp::Lt | CompareOp::Le)),
(false, true) => Ok(matches!(op, CompareOp::Ne | CompareOp::Gt | CompareOp::Ge)),
(false, false) => a.rich_compare(b, op)?.is_truthy(),
}
}

// Helper function to check if an object is a Some wrapper
fn is_some_wrapper(py: Python<'_>, obj: &Bound<'_, PyAny>) -> bool {
obj.is_instance(&py.get_type::<SomeWrapper>())
Expand Down
46 changes: 46 additions & 0 deletions src/fable-library-py/tests/test_option.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Tests for nested options, represented by `fable_library.core.option.SomeWrapper`.

Only nesting materializes a wrapper: `Some 42` and `Some (Some 42)` are the bare value,
`Some None` is `SomeWrapper(None)`. Two options of the same F# type can therefore put a
wrapper next to `None` or next to a bare value.
"""

from fable_library.option import some
from fable_library.util import compare, compare_arrays, equal_arrays, equals


def test_equality_between_different_nesting_depths() -> None:
assert (some(None) == some(some(None))) is False
assert (some(some(None)) == some(None)) is False
assert (some(None) != some(some(None))) is True
assert equals(some(None), some(some(None))) is False


def test_equality_between_equal_nesting_depths() -> None:
assert (some(None) == some(None)) is True
assert (some(some(None)) == some(some(None))) is True
assert equals(some(some(None)), some(some(None))) is True


def test_equality_against_none_and_bare_values() -> None:
assert (some(None) == None) is False # noqa: E711
assert (None == some(None)) is False # noqa: E711
assert (some(None) == 42) is False
assert (42 == some(None)) is False


def test_equality_is_inherited_by_containers() -> None:
assert equal_arrays([some(None)], [some(some(None))]) is False
assert equal_arrays([some(None)], [some(None)]) is True
assert compare_arrays([some(None)], [some(some(None))]) == -1


def test_comparison_orders_none_before_some() -> None:
assert compare(some(None), some(some(None))) == -1
assert compare(some(some(None)), some(None)) == 1
assert compare(some(None), some(None)) == 0
assert compare(None, some(None)) == -1
assert compare(some(None), None) == 1
# `int option option`: `Some None` against `Some (Some 42)`
assert compare(some(None), 42) == -1
assert compare(42, some(None)) == 1
32 changes: 32 additions & 0 deletions tests/Python/TestOption.fs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,38 @@ let ``test Option.map/map2/map3/bind/filter with nested options work`` () =
Some(Some 5) |> Option.bind (fun inner -> Some inner) |> equal (Some(Some 5))
Some(Some 5) |> Option.filter Option.isSome |> equal (Some(Some 5))

[<Fact>]
let ``test equality on nested options of different depth works`` () =
let a: int option option option = Some None
let b: int option option option = Some(Some None)
(a = b) |> equal false
(b = a) |> equal false
(a = a) |> equal true
(b = b) |> equal true
notEqual a b
let ra: Result<int option option option, string> = Ok a
let rb: Result<int option option option, string> = Ok b
(ra = rb) |> equal false
notEqual ra rb
let c: int option option = Some None
let d: int option option = Some(Some 42)
(c = d) |> equal false
(d = c) |> equal false

[<Fact>]
let ``test comparison on nested options of different depth works`` () =
let a: int option option option = Some None
let b: int option option option = Some(Some None)
compare a b |> equal -1
compare b a |> equal 1
compare a a |> equal 0
compare (None: int option option option) a |> equal -1
compare a (None: int option option option) |> equal 1
let c: int option option = Some None
let d: int option option = Some(Some 42)
compare c d |> equal -1
compare d c |> equal 1

[<Fact>]
let ``test ValueOption.map/map2/map3/bind/filter/defaultWith/orElseWith work`` () =
ValueSome 5 |> ValueOption.map (fun x -> x + 1) |> equal (ValueSome 6)
Expand Down
Loading