Coverage for src/monadc/either/either.py: 67%

76 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-19 20:24 -0700

1from typing import TypeVar, Generic, Callable, Union, Any, Optional, cast 

2 

3L = TypeVar('L') # Left type (error/failure) 

4R = TypeVar('R') # Right type (success/value) 

5U = TypeVar('U') # Generic result type 

6 

7class Either(Generic[L, R]): 

8 """ 

9 Rust and Scala inspired Either monad for representing computations that can succeed or fail. 

10 

11 Either[L, R] represents a value that can be either: 

12 - Left[L]: A failure/error value of type L 

13 - Right[R]: A success value of type R 

14 """ 

15 

16 _UNSET = object() # Sentinel value to distinguish from None 

17 

18 def __new__(cls, left: Any = _UNSET, right: Any = _UNSET) -> 'Either[L, R]': 

19 """Factory constructor for Either. Prefer explicit Right()/Left() construction.""" 

20 if cls is not Either: 

21 # Direct subclass instantiation (Left, Right) 

22 return super().__new__(cls) 

23 

24 if left is not cls._UNSET and right is not cls._UNSET: 

25 raise ValueError("Cannot specify both left and right") 

26 

27 if left is not cls._UNSET: 

28 from .left import Left 

29 return Left(left) 

30 elif right is not cls._UNSET: 

31 from .right import Right 

32 return Right(right) 

33 else: 

34 raise ValueError("Must specify either left or right") 

35 

36 # ======================================== 

37 # Rust / Scala Common API 

38 # ======================================== 

39 

40 def is_left(self) -> bool: 

41 """Returns True if this Either is a Left, False otherwise.""" 

42 raise NotImplementedError("Use Left or Right, not Either directly") 

43 

44 def is_right(self) -> bool: 

45 """Returns True if this Either is a Right, False otherwise.""" 

46 raise NotImplementedError("Use Left or Right, not Either directly") 

47 

48 def left(self) -> 'Any': # Returns Option[L] 

49 """Get the left value as Option - Some(value) if Left, Nil() if Right.""" 

50 raise NotImplementedError("Use Left or Right, not Either directly") 

51 

52 def right(self) -> 'Any': # Returns Option[R] 

53 """Get the right value as Option - Some(value) if Right, Nil() if Left.""" 

54 raise NotImplementedError("Use Left or Right, not Either directly") 

55 

56 def map(self, func: Callable[[Union[L, R]], U]) -> U: 

57 """ 

58 Map both Left and Right values to the same type. Note: 

59 - Rust: requires the same type for both left and right. 

60 - Scala: maps the right value only. 

61 - Python(monadc): blindly applies func to left or right, whichever is present. 

62 Also see `map_either` below. 

63 """ 

64 raise NotImplementedError("Use Left or Right, not Either directly") 

65 

66 def map_left(self, func: Callable[[L], U]) -> 'Either[U, R]': 

67 """Transform the Left value if present, otherwise return unchanged Right.""" 

68 raise NotImplementedError("Use Left or Right, not Either directly") 

69 

70 def map_right(self, func: Callable[[R], U]) -> 'Either[L, U]': 

71 """Transform the Right value if present, otherwise return unchanged Left.""" 

72 raise NotImplementedError("Use Left or Right, not Either directly") 

73 

74 # ======================================== 

75 # RUST API 

76 # ======================================== 

77 

78 def either(self, left_func: Callable[[L], U], right_func: Callable[[R], U]) -> U: 

79 """Apply left_func to Left value or right_func to Right value. Alias for fold.""" 

80 return self.fold(left_func, right_func) 

81 

82 def map_either(self, left_func: Callable[[L], U], right_func: Callable[[R], U]) -> U: 

83 """Transform both sides to same type using explicit left/right functions (Rust-style).""" 

84 raise NotImplementedError("Use Left or Right, not Either directly") 

85 

86 def unwrap_left(self) -> L: 

87 """Get the left value. Raises exception if this is a Right.""" 

88 raise NotImplementedError("Use Left or Right, not Either directly") 

89 

90 def unwrap_right(self) -> R: 

91 """Get the right value. Raises exception if this is a Left.""" 

92 raise NotImplementedError("Use Left or Right, not Either directly") 

93 

94 def expect_left(self, message: str) -> L: 

95 """Get the left value. Raises exception with custom message if this is a Right.""" 

96 raise NotImplementedError("Use Left or Right, not Either directly") 

97 

98 def expect_right(self, message: str) -> R: 

99 """Get the right value. Raises exception with custom message if this is a Left.""" 

100 raise NotImplementedError("Use Left or Right, not Either directly") 

101 

102 def left_or(self, other: 'Either[U, R]') -> 'Either[U, R]': 

103 """Return self if Left, otherwise return other.""" 

104 raise NotImplementedError("Use Left or Right, not Either directly") 

105 

106 def right_or(self, other: 'Either[L, U]') -> 'Either[L, U]': 

107 """Return self if Right, otherwise return other.""" 

108 raise NotImplementedError("Use Left or Right, not Either directly") 

109 

110 def left_or_else(self, func: Callable[[], 'Either[U, R]']) -> 'Either[U, R]': 

111 """Return self if Left, otherwise call func and return result.""" 

112 raise NotImplementedError("Use Left or Right, not Either directly") 

113 

114 def right_or_else(self, func: Callable[[], 'Either[L, U]']) -> 'Either[L, U]': 

115 """Return self if Right, otherwise call func and return result.""" 

116 raise NotImplementedError("Use Left or Right, not Either directly") 

117 

118 def left_and_then(self, func: Callable[[L], 'Either[U, R]']) -> 'Either[U, R]': 

119 """Transform Left value to Either if present, otherwise return unchanged Right.""" 

120 raise NotImplementedError("Use Left or Right, not Either directly") 

121 

122 def right_and_then(self, func: Callable[[R], 'Either[L, U]']) -> 'Either[L, U]': 

123 """Transform Right value to Either if present, otherwise return unchanged Left.""" 

124 raise NotImplementedError("Use Left or Right, not Either directly") 

125 

126 def flip(self) -> 'Either[R, L]': 

127 """Flip Left and Right types.""" 

128 raise NotImplementedError("Use Left or Right, not Either directly") 

129 

130 # ======================================== 

131 # SCALA API 

132 # ======================================== 

133 # NOTE: These methods favor the right part (success case) following Scala conventions. 

134 # They operate on the Right value and ignore/pass through Left values. 

135 

136 def swap(self) -> 'Either[R, L]': 

137 """Swap Left and Right types.""" 

138 raise NotImplementedError("Use Left or Right, not Either directly") 

139 

140 def fold(self, if_left: Callable[[L], U], if_right: Callable[[R], U]) -> U: 

141 """Apply if_left to Left value or if_right to Right value.""" 

142 raise NotImplementedError("Use Left or Right, not Either directly") 

143 

144 def foreach(self, func: Callable[[R], Any]) -> None: 

145 """Execute function on Right value, do nothing for Left (Scala-style).""" 

146 raise NotImplementedError("Use Left or Right, not Either directly") 

147 

148 def get(self) -> R: 

149 """Get the Right value. Raises exception if this is a Left (Scala-style).""" 

150 raise NotImplementedError("Use Left or Right, not Either directly") 

151 

152 def get_or_else(self, default: Union[R, Callable[[], R]]) -> R: 

153 """Get the Right value or return default if this is a Left (Scala-style).""" 

154 raise NotImplementedError("Use Left or Right, not Either directly") 

155 

156 def to_option(self) -> 'Any': # Returns Option[R] 

157 """Convert Either to Option, keeping Right value and losing Left information (Scala-style).""" 

158 raise NotImplementedError("Use Left or Right, not Either directly") 

159 

160 def contains(self, value: Any) -> bool: 

161 """Returns True if this is a Right containing the given value (Scala-style).""" 

162 raise NotImplementedError("Use Left or Right, not Either directly") 

163 

164 def exists(self, predicate: Callable[[R], bool]) -> bool: 

165 """Returns True if this is a Right and the predicate holds for the Right value (Scala-style).""" 

166 raise NotImplementedError("Use Left or Right, not Either directly") 

167 

168 def or_else(self, other: Union['Either[L, U]', Callable[[], 'Either[L, U]']]) -> 'Either[L, Union[R, U]]': 

169 """Return this Either if it's a Right, otherwise return other (Scala-style).""" 

170 raise NotImplementedError("Use Left or Right, not Either directly")