Coverage for /home/ubuntu/yoneda/python/yoneda/monad.py: 100%

116 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-12-03 03:56 +0000

1from typing import Any, Callable, Generic, Type, TypeVar, Union # noqa: F401 

2 

3from functools import partial 

4 

5from lunchbox.enforce import Enforce, EnforceError 

6import infix 

7 

8A = TypeVar('A') 

9B = TypeVar('B') 

10C = TypeVar('C') 

11# ------------------------------------------------------------------------------ 

12 

13 

14''' 

15Monad is a library containing the Monad class and a library of monadic 

16functions it calls. 

17 

18Haskell equivalence table: 

19 

20 =========== ======== ====== ===== ================ ================================ 

21 **Python** **Haskell** **Haskell Type Signature** 

22 -------------------- ------------ ------------------------------------------------- 

23 prefix infix prefix infix implication signature 

24 =========== ======== ====== ===== ================ ================================ 

25 app │iapp│ <*> Applicative f => f (a -> b) -> fa -> fb 

26 bind │ibind│ >>= Monad m => m a -> (a -> m b) -> m b 

27 fail │ifail│ fail Monad m => String -> m a 

28 fmap │ifmap│ fmap <$> Functor f => (a -> b) -> fa -> fb 

29 right │iright│ >> Monad m => m a -> m b -> m b 

30 unwrap Monad m => m a -> a 

31 wrap │iwrap│ pure Applicative f => a -> f a 

32 wrap │iwrap│ return Monad m => a -> m a 

33 curry │icurry│ 

34 dot │idot│ . . (b -> c) -> (a -> b) -> (a -> c) 

35 partial_dot . . (b -> c) -> (a -> b) -> (a -> c) 

36 =========== ======== ====== ===== ================ ================================ 

37''' 

38 

39 

40def enforce_monad(item): 

41 # type: (Any) -> None 

42 ''' 

43 Enforces item being a Monad subclass or instance. 

44 

45 Args: 

46 item (object): Item to be tested. 

47 

48 Raises: 

49 EnforceError: If item is not Monad subclass or instance. 

50 ''' 

51 pred = isinstance # type: Any 

52 if item.__class__ is type: 

53 pred = issubclass 

54 if not pred(item, Monad): 

55 raise EnforceError(f'{item} is not a subclass or instance of Monad.') 

56 

57 

58@infix.or_infix 

59def iwrap(*args, **kwargs): 

60 return wrap(*args, **kwargs) 

61 

62 

63def wrap(monad, data): 

64 # type: (Monadlike, A) -> Monad[A] 

65 ''' 

66 Wrap: M -> A -> MA 

67 

68 .. image:: images/wrap.png 

69 

70 Given a Monad class or instance, create a new Monad with given data. 

71 

72 Args: 

73 monad (Monad): Monad class or instance. 

74 data (Any): Data to be wrapped as Monad. 

75 

76 Raises: 

77 EnforceError: If monad is not Monad subclass or instance. 

78 

79 Returns: 

80 Monad[A]: Monad of data. 

81 ''' 

82 enforce_monad(monad) 

83 return monad.wrap(data) 

84 

85 

86def unwrap(monad): 

87 # type: (Monad[A]) -> A 

88 ''' 

89 Unwrap: MA -> A 

90 

91 .. image:: images/unwrap.png 

92 

93 Return the data of a given Monad instance. 

94 

95 Args: 

96 monad (Monad): Monad instance. 

97 

98 Raises: 

99 EnforceError: If monad is not Monad subclass or instance. 

100 

101 Returns: 

102 A: Monad data. 

103 ''' 

104 enforce_monad(monad) 

105 return monad._data 

106 

107 

108@infix.or_infix 

109def ifmap(*args, **kwargs): 

110 return fmap(*args, **kwargs) 

111 

112 

113def fmap(func, monad): 

114 # type: (Callable[[A], B], Monad[A]) -> Monad[B] 

115 ''' 

116 Functor map: (A -> B) -> MA -> MB 

117 

118 .. image:: images/fmap.png 

119 

120 Given a Monad of A (MA) and a function A to B, return a Monad of B (MB). 

121 

122 Args: 

123 func (function): Function (A -> B). 

124 monad (Monad): Monad of A. 

125 

126 Raises: 

127 EnforceError: If monad is not Monad subclass or instance. 

128 

129 Returns: 

130 Monad[B]: Monad of B. 

131 ''' 

132 enforce_monad(monad) 

133 return wrap(monad, func(unwrap(monad))) 

134 

135 

136@infix.or_infix 

137def iapp(*args, **kwargs): 

138 return app(*args, **kwargs) 

139 

140 

141def app(monad_func, monad): 

142 # type: (Monad[Callable[[A], B]], Monad[A]) -> Monad[B] 

143 ''' 

144 Applicative: M(A -> B) -> MA -> MB 

145 

146 .. image:: images/app.png 

147 

148 Given a Monad of A (MA) and a Monad of a function A to B, return a Monad 

149 of B (MB). 

150 

151 Args: 

152 monad_func (Monad): Monad of function (A -> B). 

153 monad (Monad): Monad of A. 

154 

155 Raises: 

156 EnforceError: If monad_func is not instance of Monad. 

157 EnforceError: If monad is not Monad subclass or instance. 

158 

159 Returns: 

160 Monad[B]: Monad of B. 

161 ''' 

162 enforce_monad(monad_func) 

163 enforce_monad(monad) 

164 func = unwrap(monad_func) 

165 value = unwrap(monad) 

166 return wrap(monad, func(value)) 

167 

168 

169@infix.or_infix 

170def ibind(*args, **kwargs): 

171 return bind(*args, **kwargs) 

172 

173 

174def bind(func, monad): 

175 # type: (Callable[[A], Monad[B]], Monad[A]) -> Monad[B] 

176 ''' 

177 Bind: (A -> MB) -> MA -> MB 

178 

179 .. image:: images/bind.png 

180 

181 Given a Monad of A (MA) and a function A to MB, return a Monad of B (MB). 

182 

183 Args: 

184 func (function): Function (A -> MB). 

185 monad (Monad): Monad of A. 

186 

187 Raises: 

188 EnforceError: If monad is not Monad subclass or instance. 

189 

190 Returns: 

191 Monad[B]: Monad of B. 

192 ''' 

193 enforce_monad(monad) 

194 return func(unwrap(monad)) 

195 

196 

197@infix.or_infix 

198def iright(*args, **kwargs): 

199 return right(*args, **kwargs) 

200 

201 

202def right(monad_a, monad_b): 

203 # type: (Monad[A], Monad[B]) -> Monad[B] 

204 ''' 

205 Right: MA -> MB -> MB 

206 

207 .. image:: images/right.png 

208 

209 Given two Monads, a and b, return the right Monad b. 

210 

211 Args: 

212 monad_a (Monad): Left monad. 

213 monad_b (Monad): Right monad. 

214 

215 Raises: 

216 EnforceError: If monad is not Monad subclass or instance. 

217 

218 Returns: 

219 Monad: Right Monad. 

220 ''' 

221 enforce_monad(monad_a) 

222 enforce_monad(monad_b) 

223 return monad_b 

224 

225 

226@infix.or_infix 

227def ifail(*args, **kwargs): 

228 return fail(*args, **kwargs) 

229 

230 

231def fail(monad, error): 

232 # type (Monad, Exception) -> Monad[Exception] 

233 ''' 

234 Fail: M -> E -> ME 

235 

236 .. image:: images/fail.png 

237 

238 Given a Monad and Exception, return a Monad of that Exception. 

239 

240 Args: 

241 monad (Monad): Monad to wrap error with. 

242 error (Exception): Error. 

243 

244 Raises: 

245 EnforceError: If monad is not Monad subclass or instance. 

246 EnforceError: If error is not an instance of Exception. 

247 

248 Returns: 

249 Monad: Error Monad. 

250 ''' 

251 enforce_monad(monad) 

252 msg = 'Error must be an instance of Exception. Given value: {a}.' 

253 Enforce(error, 'instance of', Exception, message=msg) 

254 return wrap(monad, error) 

255 

256 

257def succeed(monad, value): 

258 # type (Monad, A) -> Monad[A] 

259 ''' 

260 Succed: M -> A -> MA 

261 

262 .. image:: images/wrap.png 

263 

264 Given a Monad and a value, return a Monad of that value. 

265 

266 Args: 

267 monad (Monad): Monad to wrap value with. 

268 value (object): Value. 

269 

270 Raises: 

271 EnforceError: If monad is not Monad subclass or instance. 

272 EnforceError: If value is an instance of Exception. 

273 

274 Returns: 

275 Monad: Monad of value. 

276 ''' 

277 enforce_monad(monad) 

278 msg = 'Error must not be an instance of Exception. Given value: {a}.' 

279 Enforce(value, 'not instance of', Exception, message=msg) 

280 return wrap(monad, value) 

281 

282 

283@infix.or_infix 

284def icurry(*args, **kwargs): 

285 return curry(*args, **kwargs) 

286 

287 

288def curry(func, *args, **kwargs): 

289 # type: (Callable, Any, Any) -> Callable 

290 ''' 

291 Infix notation for functools.partial. 

292 

293 Args: 

294 func (function): Function to be curried. 

295 args (optional): Arguments. 

296 kwargs (optional): Keyword arguments. 

297 

298 Returns: 

299 function: Curried function. 

300 ''' 

301 return partial(func, *args, **kwargs) 

302 

303 

304@infix.or_infix 

305def idot(*args, **kwargs): 

306 return dot(*args, **kwargs) 

307 

308 

309def dot(func_b, func_a): 

310 # type: (Callable[[B], C], Callable[[A], B]) -> Callable[[A], C] 

311 ''' 

312 Dot: (b -> c) -> (a -> b) -> (a -> c) 

313 fb |idot| fa == fb(fa) 

314 

315 Composes two functions. 

316 

317 Example: 

318 ``` 

319 fa = lambda x: x + 'a' 

320 fb = lambda x: x + 'b' 

321 dot(fb, fa)('x') == 'xab' 

322 (fb | idot | fa)('x') == 'xab' 

323 ``` 

324 

325 Args: 

326 func_b (function): Outer function. 

327 func_a (function): Inner function. 

328 

329 Returns: 

330 partial: Function composition. 

331 ''' 

332 

333 def of(b, a, *args, **kwargs): 

334 return b(a(*args, **kwargs)) 

335 

336 return partial(of, func_b, func_a) 

337 

338 

339# TODO: Figure out how to make this type signature work 

340def partial_dot(func): 

341 # type: (Callable[[B], C]) -> Any 

342 ''' 

343 Partial Dot: (b -> c) -> (a -> b) 

344 

345 Partial version of dot function. 

346 

347 Example: 

348 ``` 

349 app = ym.app 

350 u = Monad(lambda x: x + 1) 

351 v = Monad(lambda x: x + 2) 

352 w = Monad(3) 

353 Monad(partial_dot) | iapp | u | iapp | v | iapp | w 

354 ``` 

355 

356 Args: 

357 func (function): Outer composition function. 

358 

359 Returns: 

360 partial: Function composition. 

361 ''' 

362 return partial(dot, func) 

363 

364 

365def catch(monad, func): 

366 # type: (MA, Callable[[A], B]) -> Callable[[A], Union[B, Exception]] 

367 ''' 

368 Catch: MA -> (A -> B) -> (MB | ME) 

369 

370 Catches exception and returns it rather then raising an error. 

371 

372 Args: 

373 monad (Monad): Monad. 

374 func (function): Function to attempt. 

375 

376 Raises: 

377 EnforceError: If monad is not Monad subclass or instance. 

378 

379 Returns: 

380 object: Partial function with catch logic. 

381 ''' 

382 enforce_monad(monad) 

383 

384 def catch_(func, *args, **kwargs): 

385 try: 

386 return func(*args, **kwargs) 

387 except Exception as error: 

388 return fail(monad, error) 

389 

390 return partial(catch_, func) 

391 

392 

393# ------------------------------------------------------------------------------ 

394 

395 

396class Monad(Generic[A]): 

397 ''' 

398 Monad is a generic base class for monads. It implements all the monad 

399 functions as methods which take itself as the first argument. 

400 

401 Haskell equivalence table: 

402 

403 ====== ===== ====== ===== ================ ======================== 

404 **Python** **Haskell** **Haskell Type Signature** 

405 ------------ ------------ ----------------------------------------- 

406 prefix infix prefix infix implication signature 

407 ====== ===== ====== ===== ================ ======================== 

408 app ^ <*> Applicative f => f (a -> b) -> fa -> fb 

409 bind >> >>= Monad m => m a -> (a -> m b) -> m b 

410 fail fail Monad m => String -> m a 

411 fmap & fmap <$> Functor f => (a -> b) -> fa -> fb 

412 right >> Monad m => m a -> m b -> m b 

413 unwrap Monad m => m a -> a 

414 wrap pure Applicative f => a -> f a 

415 wrap return Monad m => a -> m a 

416 ====== ===== ====== ===== ================ ======================== 

417 ''' 

418 

419 def __init__(self, data): 

420 # type: (A) -> None 

421 ''' 

422 Constructs monad instance. 

423 

424 Args: 

425 data (object): Data to be wrapped with Monad. 

426 ''' 

427 self._data = data 

428 

429 def __repr__(self): 

430 # type: () -> str 

431 ''' 

432 String representation of Monad instance. 

433 ''' 

434 return f'{self.__class__.__name__}({self._data.__repr__()})' 

435 

436 @classmethod 

437 def wrap(cls, data): 

438 # type: (A) -> MA 

439 ''' 

440 Wrap: A -> MA 

441 

442 Create a new Monad with given data. 

443 

444 Args: 

445 data (Any): Data to be wrapped as Monad. 

446 

447 Returns: 

448 Monad[A]: Monad of data. 

449 ''' 

450 return cls(data) 

451 

452 def unwrap(self): 

453 # type: () -> A 

454 ''' 

455 Unwrap: () -> A 

456 

457 Return self._data. 

458 

459 Returns: 

460 A: Monad data. 

461 ''' 

462 return unwrap(self) 

463 

464 def fmap(self, func): 

465 # type: (Callable[[A], B]) -> MB 

466 ''' 

467 Functor map: (A -> B) -> MB 

468 

469 Given a function A to B, return a Monad of B (MB). 

470 Example: m.fmap(lambda x: x + 2) 

471 

472 Args: 

473 func (function): Function (A -> B). 

474 

475 Returns: 

476 Monad[B]: Monad of B. 

477 ''' 

478 return fmap(func, self) 

479 

480 def app(self, monad_func): 

481 # type: (Monad[Callable[[A], B]]) -> MB 

482 ''' 

483 Applicative: M(A -> B) -> MB 

484 

485 Given a Monad of a function A to B, return a Monad of B (MB). 

486 

487 Args: 

488 monad_func (Monad): Monad of function (A -> B). 

489 

490 Returns: 

491 Monad[B]: Monad of B. 

492 ''' 

493 return app(monad_func, self) 

494 

495 def bind(self, func): 

496 # type: (Callable[[A], MB]) -> MB 

497 ''' 

498 Bind: (A -> MB) -> MB 

499 

500 Given a function A to MB, return a Monad of B (MB). 

501 

502 Args: 

503 func (function): Function (A -> MB). 

504 

505 Returns: 

506 Monad[B]: Monad of B. 

507 ''' 

508 return bind(func, self) 

509 

510 def right(self, monad): 

511 # type: (MB) -> MB 

512 ''' 

513 Right: MB -> MB 

514 

515 Return given monad (self is left, given monad is right). 

516 

517 Args: 

518 monad (Monad): Right monad. 

519 

520 Returns: 

521 Monad: Right Monad. 

522 ''' 

523 return right(self, monad) 

524 

525 def fail(self, error): 

526 # type (Exception) -> Monad[Exception] 

527 ''' 

528 Fail: E -> ME 

529 

530 Return a Monad of given Exception. 

531 

532 Args: 

533 error (Exception): Error. 

534 

535 Returns: 

536 Monad: Error Monad. 

537 ''' 

538 return fail(self, error) 

539 

540 def __and__(self, func): 

541 # type: (Callable[[A], B]) -> MB 

542 ''' 

543 Functor map: (A -> B) -> MB 

544 

545 Given a function A to B, return a Monad of B (MB). 

546 Example: m & (lambda x: x + 2) 

547 

548 Args: 

549 func (function): Function (A -> B). 

550 

551 Returns: 

552 Monad[B]: Monad of B. 

553 ''' 

554 return self.fmap(func) 

555 

556 def __xor__(self, monad_func): 

557 # type: (MA, Monad[Callable[[A], B]]) -> MB 

558 ''' 

559 Applicative: MA -> M(A -> B) -> MB 

560 

561 .. image:: images/app.png 

562 

563 Given a Monad of A (MA) and a Monad of a function A to B, return a Monad 

564 of B (MB). 

565 Example: m ^ Monad.wrap(lambda x: x + 2) 

566 

567 Args: 

568 monad (Monad): Monad of A. 

569 func (Monad): Monad of function (A -> B). 

570 

571 Raises: 

572 EnforceError: If monad is not Monad subclass or instance. 

573 

574 Returns: 

575 Monad[B]: Monad of B. 

576 ''' 

577 return self.app(monad_func) 

578 

579 def __rshift__(self, func): 

580 # type: (Callable[[A], MB]) -> MB 

581 ''' 

582 Bind: (A -> MB) -> MB 

583 

584 Given a function A to MB, return a Monad of B (MB). 

585 Example: m >> Monad 

586 

587 Args: 

588 func (function): Function (A -> MB). 

589 

590 Returns: 

591 Monad[B]: Monad of B. 

592 ''' 

593 return self.bind(func) 

594 

595 

596Monadlike = Union[Monad, Type[Monad]] 

597MA = Monad[A] 

598MB = Monad[B] 

599MC = Monad[C]