"""Maybe, Result, and reusable Reader with concrete law checks. Python 3.10+."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable


@dataclass(frozen=True)
class Just:
    value: object

    def bind(self, function):
        """Pre: continuation returns Maybe. Post: apply it to the present value."""
        result = function(self.value)
        if not isinstance(result, (Just, Nothing)):
            raise TypeError("Maybe.bind continuation must return Maybe")
        return result

    def map(self, function):
        return self.bind(lambda value: Just(function(value)))


@dataclass(frozen=True)
class Nothing:
    def bind(self, function):
        """Post: keep absence without calling the continuation."""
        return self

    def map(self, function):
        return self


@dataclass(frozen=True)
class Ok:
    value: object

    def bind(self, function):
        result = function(self.value)
        if not isinstance(result, (Ok, Err)):
            raise TypeError("Result.bind continuation must return Result")
        return result


@dataclass(frozen=True)
class Err:
    message: str

    def bind(self, function):
        return self


@dataclass(frozen=True)
class Reader:
    run: Callable

    @staticmethod
    def unit(value):
        return Reader(lambda environment: value)

    def bind(self, function):
        """Post: use the same environment throughout each independent run."""
        return Reader(lambda environment: function(self.run(environment)).run(environment))


def parse_integer(text):
    """Post: Ok(integer) or Err with an explanation."""
    try:
        return Ok(int(text))
    except ValueError:
        return Err("not an integer")


def reciprocal(value):
    """Pre: numeric input. Post: Ok(1/value), or Err for zero."""
    return Err("division by zero") if value == 0 else Ok(1/value)


def demo():
    print("Successful result:", parse_integer("4").bind(reciprocal))
    print("Invalid input:", parse_integer("bee").bind(reciprocal))
    print("Zero input:", parse_integer("0").bind(reciprocal))
    print("A present None differs from absence:", Just(None), Nothing())
    ask = Reader(lambda environment: environment["greeting"])
    greeting = ask.bind(lambda word: Reader.unit(word + ", Ada"))
    print("Reusable Reader:", greeting.run({"greeting": "Hello"}), greeting.run({"greeting": "Hola"}))


def self_test():
    count = 0
    f, g = lambda x: Just(x+1), lambda x: Just(x*3)
    # Invariant: previous representative values satisfy the three laws. Bound: remaining cases.
    for value in range(-5, 6):
        m = Just(value)
        check(Just(value).bind(f), f(value))
        check(m.bind(Just), m)
        check(m.bind(f).bind(g), m.bind(lambda x: f(x).bind(g)))
        count += 3
    check(Nothing().bind(lambda x: 1/0), Nothing())
    check(parse_integer("0").bind(reciprocal), Err("division by zero"))
    reader = Reader(lambda env: env["x"]).bind(lambda x: Reader.unit(x+1))
    check(reader.run({"x": 1}), 2)
    check(reader.run({"x": 9}), 10)
    check(reader.run({"x": 1}), 2)
    return count + 5

def check(actual, expected):
    """Require an expected result; this check also runs under python -O."""
    if actual != expected:
        raise AssertionError(f"expected {expected!r}, got {actual!r}")


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--self-test", action="store_true")
    args = parser.parse_args()
    if args.self_test:
        print(f"{self_test()} checks passed")
    else:
        demo()
