├── .gitignore ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── f ├── __init__.py ├── collection.py ├── function.py ├── generic.py ├── monad.py └── predicate.py ├── pip.requirements ├── pip.requirements.dev ├── pip.requirements.test ├── setup.cfg ├── setup.py ├── tests ├── test_collection.py ├── test_function.py ├── test_generic.py ├── test_monad.py └── test_predicate.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | *.pyc 3 | .tox 4 | *.egg-info 5 | dist 6 | README.html 7 | build 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7 2 | ENV PYTHONDONTWRITEBYTECODE 1 3 | ENV PYTHONPATH /app 4 | 5 | COPY pip.requirements* / 6 | WORKDIR / 7 | RUN pip install -r pip.requirements.dev 8 | RUN rm pip.requirements* 9 | 10 | WORKDIR /app 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Ivan Grishaev ivan@grishaev.me 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=. py.test tests/ -s -x -v 4 | 5 | pep: 6 | flake8 . 7 | 8 | clean: 9 | find . -name "*.pyc" -delete 10 | 11 | build: 12 | python setup.py bdist_wheel --universal 13 | 14 | register: 15 | python setup.py register -r pypi 16 | 17 | upload: 18 | python setup.py bdist_wheel --universal upload -r pypi 19 | 20 | docker-build: 21 | docker build -t f:tests . 22 | 23 | docker-run: 24 | docker run -it --rm -p 8080:8080 -v $(CURDIR):/app f:tests bash 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `f` is a set of functional tools for Python 2 | 3 | ## Functions 4 | 5 | A bunch of useful functions to work with data structures. 6 | 7 | ### Protected call (comes from Lua): 8 | 9 | ```python 10 | import f 11 | 12 | f.pcall(lambda a, b: a / b, 4, 2) 13 | >>> (None, 2) 14 | 15 | f.pcall(lambda a, b: a / b, 4, 0) 16 | >>> (ZeroDivisionError('integer division or modulo by zero'), None) 17 | ``` 18 | 19 | Or use it like a decorator: 20 | 21 | ```python 22 | 23 | @f.pcall_wraps 24 | def func(a, b): 25 | return a / b 26 | 27 | func(4, 2) 28 | >>> (None, 2) 29 | 30 | func(4, 0) 31 | >>> (ZeroDivisionError('integer division or modulo by zero'), None) 32 | 33 | ``` 34 | 35 | ### Attribute and item chain functions handling exceptions: 36 | 37 | ```python 38 | # let's say, you have a schema with the following foreign keys: 39 | # Order --> Office --> Department --> Chief 40 | 41 | order = Order.objects.get(id=42) 42 | 43 | # OK 44 | f.achain(model, 'office', 'department', 'chief', 'name') 45 | >>> John 46 | 47 | # Now imagine the `department` field is null-able and has NULL in the DB: 48 | f.achain(model, 'office', 'department', 'chief', 'name') 49 | >>> None 50 | ``` 51 | 52 | ```python 53 | data = json.loads('{"result": [{"kids": [{"age": 7, "name": "Leo"}, {"age": 1, "name": "Ann"}], "name": "Ivan"}, {"kids": null, "name": "Juan"}]}') 54 | 55 | # OK 56 | f.ichain(data, 'result', 0, 'kids', 0, 'age') 57 | >>> 7 58 | 59 | # the chain is broken 60 | f.ichain(data, 'result', 42, 'kids', 0, 'age') 61 | >> None 62 | ``` 63 | 64 | ### Threading functions from Clojure 65 | 66 | The first threading macro puts the value into an each form as a first 67 | argument to a function: 68 | 69 | ```python 70 | f.arr1( 71 | -42, # initial value 72 | (lambda a, b: a + b, 2), # form 73 | abs, # form 74 | str, # form 75 | (str.replace, "40", "__") # form 76 | ) 77 | >>> "__" 78 | ``` 79 | 80 | The second threading macro is just the same, but puts a value at the end: 81 | 82 | ```python 83 | f.arr2( 84 | -2, 85 | abs, 86 | (lambda a, b: a + b, 2), 87 | str, 88 | ("000".replace, "0") 89 | ) 90 | >>> "444" 91 | ``` 92 | 93 | ### Function composition: 94 | 95 | ```python 96 | comp = f.comp(abs, (lambda x: x * 2), str) 97 | comp(-42) 98 | >>> "84" 99 | ``` 100 | 101 | ### Every predicate 102 | 103 | Composes a super predicate from the passed ones: 104 | 105 | ```python 106 | pred1 = f.p_gt(0) 107 | pred2 = f.p_even 108 | pred3 = f.p_not_eq(666) 109 | 110 | every = f.every_pred(pred1, pred2, pred3) 111 | 112 | result = filter(every, (-1, 1, -2, 2, 3, 4, 666, -3, 1, 2)) 113 | tuple(result) 114 | >>> (2, 4, 2) 115 | ``` 116 | 117 | ### Transducer: a quick-and-dirty port from Clojure 118 | 119 | ```python 120 | f.transduce( 121 | (lambda x: x + 1), 122 | (lambda res, item: res + str(item)), 123 | (1, 2, 3), 124 | "" 125 | ) 126 | >>> "234" 127 | ``` 128 | 129 | ### Nth element getters 130 | 131 | ```python 132 | f.first((1, 2, 3)) 133 | >>> 1 134 | 135 | f.second((1, 2, 3)) 136 | >>> 2 137 | 138 | f.third((1, 2, 3)) 139 | >>> 3 140 | 141 | f.nth(0, [1, 2, 3]) 142 | >>> 1 143 | 144 | f.nth(9, [1, 2, 3]) 145 | >>> None 146 | ``` 147 | 148 | ## Predicates 149 | 150 | A set of unary and binary predicates. 151 | 152 | Unary example: 153 | 154 | ```python 155 | f.p_str("test") 156 | >>> True 157 | 158 | f.p_str(0) 159 | >>> False 160 | 161 | f.p_str(u"test") 162 | >>> True 163 | 164 | # checks for both int and float types 165 | f.p_num(1), f.p_num(1.0) 166 | >>> True, True 167 | 168 | f.p_list([]) 169 | >>> True 170 | 171 | f.p_truth(1) 172 | >>> True 173 | 174 | f.p_truth(None) 175 | >>> False 176 | 177 | f.p_none(None) 178 | >>> True 179 | ``` 180 | 181 | Binary example: 182 | 183 | ```python 184 | p = f.p_gt(0) 185 | 186 | p(1), p(100), p(0), p(-1) 187 | >>> True, True, False, False 188 | 189 | p = f.p_gte(0) 190 | p(0), p(1), p(-1) 191 | >>> True, True, False 192 | 193 | p = f.p_eq(42) 194 | p(42), p(False) 195 | >>> True, False 196 | 197 | ob1 = object() 198 | p = f.p_is(ob1) 199 | p(object()) 200 | >>> False 201 | p(ob1) 202 | >>> True 203 | 204 | p = f.p_in((1, 2, 3)) 205 | p(1), p(3) 206 | >>> True, True 207 | p(4) 208 | >>> False 209 | ``` 210 | 211 | You may combine predicates with `f.comp` or `f.every_pred`: 212 | 213 | ```python 214 | # checks for positive even number 215 | pred = f.every_pred(f.p_num, f.p_even, f.p_gt(0)) 216 | pred(None), pred(-1), pred(5) 217 | >>> False, False, False 218 | pred(6) 219 | >>> True 220 | ``` 221 | 222 | ## Collections 223 | 224 | Improved collections `List`, `Tuple`, `Dict` and `Set` with the following 225 | features. 226 | 227 | ### Square braces syntax for initiating 228 | 229 | ```python 230 | f.List[1, 2, 3] # or just f.L 231 | >>> List[1, 2, 3] 232 | 233 | f.T[1, 2, 3] 234 | >>> Tuple(1, 2, 3) 235 | 236 | f.Set[1, 2, 3] 237 | >>> Set{1, 2, 3} 238 | 239 | f.D[1: 2, 2: 3] 240 | >>> Dict{1: 2, 2: 3} 241 | ``` 242 | 243 | ### Additional methods such as .map, .filter, .foreach, .sum, etc: 244 | 245 | ```python 246 | l1 = f.L[1, 2, 3] 247 | l1.map(str).join("-") 248 | >>> "1-2-3" 249 | 250 | result = [] 251 | 252 | def collect(x, delta=0): 253 | result.append(x + delta) 254 | 255 | l1.foreach(collect, delta=1) 256 | result == [2, 3, 4] 257 | >>> True 258 | ``` 259 | 260 | See the source code for more methods. 261 | 262 | ### Every method returns a new collection of this type: 263 | 264 | ```python 265 | l1.filter(f.p_even) 266 | >>> List[2] 267 | 268 | l1.group(2) 269 | >>> List[List[1, 2], List[3]] 270 | 271 | # filtering a dict: 272 | f.D[1: 1, 2: 2, 0: 2].filter(lambda (k, v): k + v == 2) 273 | >>> Dict{0: 2, 1: 1} 274 | ``` 275 | 276 | ### Easy adding two collection of different types 277 | 278 | ```python 279 | 280 | # merging dicts 281 | f.D(a=1, b=2, c=3) + {"d": 4, "e": 5, "f": 5} 282 | >>> Dict{'a': 1, 'c': 3, 'b': 2, 'e': 5, 'd': 4, 'f': 5} 283 | 284 | f.S[1, 2, 3] + ["a", 1, "b", 3, "c"] 285 | >>> Set{'a', 1, 2, 3, 'c', 'b'} 286 | 287 | # adding list with tuple 288 | f.L[1, 2, 3] + (4, ) 289 | List[1, 2, 3, 4] 290 | ``` 291 | 292 | ### Quick turning to another collection 293 | 294 | ```python 295 | f.L["a", 1, "b", 2].group(2).D() 296 | >>> Dict{"a": 1, "b": 2} 297 | 298 | f.L[1, 2, 3, 3, 2, 1].S().T() 299 | >>> Tuple[1, 2, 3] 300 | ``` 301 | 302 | ## Monads 303 | 304 | There are Maybe, Either, Error and IO monads are in the library. Most of them 305 | are based on classical Haskell definitions. The main difference is they use 306 | predicates instead of type checks. 307 | 308 | I had to implement `>>=` operator as `>>` (right binary shift). There is also a 309 | Python-specific `.get()` method to fetch an actual value from a monadic 310 | instance. Be fair and use it only at the end of the monadic computation! 311 | 312 | ### Maybe 313 | 314 | ```python 315 | # Define a monadic constructor 316 | MaybeInt = f.maybe(f.p_int) 317 | 318 | MaybeInt(2) 319 | >>> Just[2] 320 | 321 | MaybeInt("not an int") 322 | >>> Nothing 323 | 324 | # Monadic pipeline 325 | MaybeInt(2) >> (lambda x: MaybeInt(x + 2)) 326 | >>> Just[4] 327 | 328 | # Nothing breaks the pipeline 329 | MaybeInt(2) >> (lambda x: f.Nothing()) >> (lambda x: MaybeInt(x + 2)) 330 | >>> Nothing 331 | ``` 332 | 333 | The better way to engage monads into you project is to use monadic decorators: 334 | 335 | ```python 336 | @f.maybe_wraps(f.p_num) 337 | def mdiv(a, b): 338 | if b: 339 | return a / b 340 | else: 341 | return None 342 | 343 | mdiv(4, 2) 344 | >>> Just[2] 345 | 346 | mdiv(4, 0) 347 | >>> Nothing 348 | ``` 349 | 350 | Use `.bind` method as an alias to `>>`: 351 | 352 | ```python 353 | 354 | MaybeInt(2).bind(lambda x: MaybeInt(x + 1)) 355 | >>> Just[3] 356 | ``` 357 | 358 | You may pass additional arguments to both `.bind` and `>>` methods: 359 | 360 | ```python 361 | MaybeInt(6) >> (mdiv, 2) 362 | >>> Just[3] 363 | 364 | MaybeInt(6).bind(mdiv, 2) 365 | >>> Just[3] 366 | ``` 367 | 368 | Release the final value: 369 | 370 | ```python 371 | m = MaybeInt(2) >> (lambda x: MaybeInt(x + 2)) 372 | m.get() 373 | >>> 3 374 | ``` 375 | 376 | ### Either 377 | 378 | This monad presents two possible values: Left (negative) and Right 379 | (positive). 380 | 381 | ```python 382 | # create a constructor based on left and right predicates. 383 | EitherStrNum = f.either(f.p_str, f.p_num) 384 | 385 | EitherStrNum("error") 386 | >>> Left[error] 387 | 388 | EitherStrNum(42) 389 | >>> Right[42] 390 | ``` 391 | 392 | Right value follows the pipeline, but Left breaks it. 393 | 394 | ```python 395 | EitherStrNum(1) >> (lambda x: EitherStrNum(x + 1)) 396 | >>> Right[2] 397 | 398 | EitherStrNum(1) >> (lambda x: EitherStrNum("error")) >> (lambda x: EitherStrNum(x + 1)) 399 | >>> Left[error] 400 | ``` 401 | 402 | When the plain value does not fit both predicates, `TypeError` occurs: 403 | 404 | ```python 405 | EitherStrNum(None) 406 | >>> TypeError: Value None doesn't fit... 407 | ``` 408 | 409 | Use decorator to wrap an existing function with Either logic: 410 | 411 | ```python 412 | @f.either_wraps(f.p_str, f.p_num) 413 | def ediv(a, b): 414 | if b == 0: 415 | return "Div by zero: %s / %s" % (a, b) 416 | else: 417 | return a / b 418 | 419 | 420 | @f.either_wraps(f.p_str, f.p_num) 421 | def esqrt(a): 422 | if a < 0: 423 | return "Negative number: %s" % a 424 | else: 425 | return math.sqrt(a) 426 | 427 | 428 | EitherStrNum(16) >> (ediv, 4) >> esqrt 429 | >>> Right[2.0] 430 | 431 | EitherStrNum(16) >> (ediv, 0) >> esqrt 432 | >>> Left[Div by zero: 16 / 0] 433 | ``` 434 | 435 | ### IO 436 | 437 | This monad wraps a function that does I/O operations. All the further calls 438 | return monadic instances of the result. 439 | 440 | ``` 441 | IoPrompt = f.io(lambda prompt: raw_input(prompt)) 442 | IoPrompt("Your name: ") # prompts for you name, I'll type "Ivan" and RET 443 | >>> IO[Ivan] 444 | ``` 445 | 446 | Or use decorator: 447 | 448 | ```python 449 | import sys 450 | 451 | @f.io_wraps 452 | def input(msg): 453 | return raw_input(msg) 454 | 455 | @f.io_wraps 456 | def write(text, chan): 457 | chan.write(text) 458 | 459 | input("name: ") >> (write, sys.stdout) 460 | >>> name: Ivan 461 | >>> Ivan 462 | >>> IO[None] 463 | ``` 464 | 465 | ### Error 466 | 467 | Error monad also known as `Try` in Scala is to prevent rising 468 | exceptions. Instead, it provides `Success` sub-class to wrap positive result and 469 | `Failture` to wrap an occured exception. 470 | 471 | ``` 472 | Error = f.error(lambda a, b: a / b) 473 | 474 | Error(4, 2) 475 | >>> Success[2] 476 | 477 | Error(4, 0) 478 | >>> Failture[integer division or modulo by zero] 479 | ``` 480 | 481 | Getting a value from `Failture` with `.get` method will re-rise it. Use 482 | `.recover` method to deal with exception in a safe way. 483 | 484 | ```python 485 | Error(4, 0).get() 486 | ZeroDivisionError: integer division or modulo by zero 487 | 488 | # value variant 489 | Error(4, 0).recover(ZeroDivisionError, 42) 490 | Success[2] 491 | ``` 492 | 493 | You may pass a tuple of exception classes. A value might be a function that 494 | takes a exception instance and returns a proper value: 495 | 496 | ```python 497 | 498 | def handler(e): 499 | logger.exception(e) 500 | return 0 501 | 502 | Error(4, 0).recover((ZeroDivisionError, TypeError), handler) 503 | >>> Success[0] 504 | ``` 505 | 506 | Decorator variant: 507 | 508 | ```python 509 | @f.error_wraps 510 | def tdiv(a, b): 511 | return a / b 512 | 513 | 514 | @f.error_wraps 515 | def tsqrt(a): 516 | return math.sqrt(a) 517 | 518 | tdiv(16, 4) >> tsqrt 519 | >>> Success[2.0] 520 | 521 | tsqrt(16).bind(tdiv, 2) 522 | >>> Success[2.0] 523 | ``` 524 | 525 | ## Generics 526 | 527 | Generic is a flexible callable object that may have different strategies 528 | depending on a set of predicates (guards). 529 | 530 | ```python 531 | # Create an instance 532 | gen = f.Generic() 533 | 534 | # extend it with handlers 535 | @gen.extend(f.p_int, f.p_str) 536 | def handler1(x, y): 537 | return str(x) + y 538 | 539 | @gen.extend(f.p_int, f.p_int) 540 | def handler2(x, y): 541 | return x + y 542 | 543 | @gen.extend(f.p_str, f.p_str) 544 | def handler3(x, y): 545 | return x + y + x + y 546 | 547 | @gen.extend(f.p_str) 548 | def handler4(x): 549 | return "-".join(reversed(x)) 550 | 551 | @gen.extend() 552 | def handler5(): 553 | return 42 554 | 555 | @gen.extend(f.p_none) 556 | def handler6(x): 557 | return gen(1, 2) 558 | 559 | # let's try: 560 | gen(None) 561 | >>> 3 562 | 563 | gen(1, "2") 564 | >>> "12" 565 | 566 | gen(1, 2) 567 | >>> 3 568 | 569 | gen("fiz", "baz") 570 | >>> "fizbazfizbaz" 571 | 572 | gen("hello") 573 | >>> "o-l-l-e-h" 574 | 575 | gen() 576 | >>> 42 577 | 578 | # calling without a default handler 579 | gen(1, 2, 3, 4) 580 | >>> TypeError exception goes here... 581 | 582 | # now we have one 583 | @gen.default 584 | def default_handler(*args): 585 | return "default" 586 | 587 | gen(1, 2, 3, 4) 588 | >>> "default" 589 | ``` 590 | -------------------------------------------------------------------------------- /f/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = (0, 0, 1) 3 | __author__ = "Ivan Grishaev" 4 | __email__ = "ivan@grishaev.me" 5 | 6 | from .function import * # noqa 7 | from .collection import * # noqa 8 | from .monad import * # noqa 9 | from .generic import * # noqa 10 | from .predicate import * # noqa 11 | -------------------------------------------------------------------------------- /f/collection.py: -------------------------------------------------------------------------------- 1 | 2 | import six 3 | 4 | __all__ = ( 5 | 'List', 6 | 'Tuple', 7 | 'Set', 8 | 'Dict', 9 | 'L', 10 | 'T', 11 | 'S', 12 | 'D', 13 | ) 14 | 15 | range = six.moves.range 16 | filter = six.moves.filter 17 | 18 | 19 | class SeqMixin(object): 20 | """ 21 | A basic mixin for all types of collections. 22 | 23 | It provides additional methods and changes 24 | collection's behaviour. 25 | """ 26 | 27 | def join(self, sep=""): 28 | """ 29 | Joins a collection into a string. 30 | 31 | Note: turn items to strings before using map: 32 | >>> coll.map(str).join(',') 33 | """ 34 | return sep.join(self) 35 | 36 | def foreach(self, func, *args, **kwargs): 37 | """ 38 | Performs a function with additional arguments 39 | for each element without returning a result. 40 | """ 41 | for item in self: 42 | func(item, *args, **kwargs) 43 | 44 | def map(self, fn, *args, **kwargs): 45 | """ 46 | Returns a new collection of this type 47 | without changing an existing one. 48 | 49 | Each element is a result of applying a passed function 50 | to corresponding element of an old collection. 51 | """ 52 | def process(item): 53 | return fn(item, *args, **kwargs) 54 | 55 | return self.__class__(map(process, self)) 56 | 57 | def filter(self, pred, *args, **kwargs): 58 | 59 | def criteria(item): 60 | return pred(item, *args, **kwargs) 61 | 62 | return self.__class__(filter(criteria, self)) 63 | 64 | def reduce(self, init, fn, *args, **kwargs): 65 | """ 66 | Returns a new filtered collection. 67 | """ 68 | def reducer(res, item): 69 | return fn(res, item, *args, **kwargs) 70 | 71 | return six.moves.reduce(reducer, self, init) 72 | 73 | def sum(self): 74 | """ 75 | Adds all the elements together. 76 | 77 | Turn items to numbers first using map: 78 | >>> coll.map(int).sum() 79 | """ 80 | return sum(self) 81 | 82 | def __add__(self, other): 83 | """ 84 | Adds a two collection together. Returns a new collection 85 | without changing old ones. 86 | 87 | The other collection might be a different type. 88 | It will be converted to the current type. 89 | """ 90 | def gen(): 91 | for x in self: 92 | yield x 93 | for y in self.__class__(other): 94 | yield y 95 | 96 | return self.__class__(gen()) 97 | 98 | def __repr__(self): 99 | """ 100 | Just like a parent __repr__, but with a leading prefix: 101 | >>> List[1, 2, 3], Dict{1: 2, 3: 4}, etc 102 | """ 103 | old_repr = super(SeqMixin, self).__repr__() 104 | return "%s%s" % (self.__class__.__name__, old_repr) 105 | 106 | # Shortcuts to turn a collection into another type. 107 | def Tuple(self): 108 | return Tuple(self) 109 | 110 | def List(self): 111 | return List(self) 112 | 113 | def Set(self): 114 | return Set(self) 115 | 116 | def Dict(self): 117 | return Dict(self) 118 | 119 | L = List 120 | T = Tuple 121 | S = Set 122 | D = Dict 123 | 124 | 125 | class LTmixin(object): 126 | """ 127 | List and Tuple features. 128 | """ 129 | 130 | def __getitem__(self, item): 131 | """ 132 | Performs accessing value by index. 133 | 134 | If a slice was passed, turn the result 135 | into the current collection type. 136 | """ 137 | result = super(LTmixin, self).__getitem__(item) 138 | 139 | if isinstance(item, slice): 140 | return self.__class__(result) 141 | else: 142 | return result 143 | 144 | # PY2 only method 145 | def __getslice__(self, *args): 146 | result = super(LTmixin, self).__getslice__(*args) 147 | return self.__class__(result) 148 | 149 | def reversed(self): 150 | """ 151 | Returns a new collection in a reserved order. 152 | """ 153 | return self.__class__(reversed(self)) 154 | 155 | def sorted(self, key=None): 156 | """ 157 | Returns a new sorted collection. 158 | 159 | Without passing a key functions, it returns as-is. 160 | A key function is a two-argument function that determes 161 | sorting logic. 162 | """ 163 | return self.__class__(sorted(self, key=key)) 164 | 165 | def group(self, n=2): 166 | """ 167 | Returns a new collections with items grouped by N: 168 | >>> L[1, 2, 3].group(2) ==> L[L[1, 2], L[3]] 169 | """ 170 | gen = (self[i: i+n] for i in range(0, len(self), n)) 171 | return self.__class__(gen) 172 | 173 | def distinct(self): 174 | """ 175 | Returns a new collection without duplicates. 176 | 177 | We don't use set(...) trick here to keep previous order. 178 | It supports non-hashable elements such as dicts or lists. 179 | """ 180 | 181 | cache_hash = set() 182 | cache_list = [] 183 | 184 | def is_hashable(x): 185 | return getattr(x, '__hash__', None) is not None 186 | 187 | def cache_set(x): 188 | if is_hashable(x): 189 | cache_hash.add(x) 190 | else: 191 | cache_list.append(x) 192 | 193 | def cache_has(x): 194 | if is_hashable(x): 195 | return x in cache_hash 196 | else: 197 | return x in cache_list 198 | 199 | res = [] 200 | 201 | def process(x): 202 | if not cache_has(x): 203 | cache_set(x) 204 | res.append(x) 205 | 206 | self.foreach(process) 207 | 208 | return self.__class__(res) 209 | 210 | def apply(self, fn): 211 | """ 212 | Passes all the items as *args to a function 213 | and gets the result. 214 | """ 215 | return fn(*self) 216 | 217 | 218 | class LTSMeta(type): 219 | 220 | def __getitem__(cls, args): 221 | """ 222 | Triggers when accessing a collection class by index, e.g: 223 | >>> List[1, 2, 3] 224 | """ 225 | if isinstance(args, tuple): 226 | return cls(args) 227 | else: 228 | return cls((args, )) 229 | 230 | 231 | @six.add_metaclass(LTSMeta) 232 | class LTSmixin(object): 233 | """ 234 | List, Tuple and Set features. 235 | """ 236 | pass 237 | 238 | 239 | class List(SeqMixin, LTSmixin, LTmixin, list): 240 | pass 241 | 242 | 243 | class Tuple(SeqMixin, LTSmixin, LTmixin, tuple): 244 | pass 245 | 246 | 247 | class Set(SeqMixin, LTSmixin, set): 248 | 249 | def __repr__(self): 250 | old_repr = set.__repr__(self) 251 | return "Set{%s}" % old_repr[5:-2] 252 | 253 | 254 | class DictMeta(type): 255 | 256 | def __getitem__(cls, slices): 257 | """ 258 | Builds a dict having a tuple of slices. 259 | """ 260 | 261 | if isinstance(slices, tuple): 262 | slice_tuple = slices 263 | else: 264 | slice_tuple = (slices, ) 265 | 266 | keys = (sl.start for sl in slice_tuple) 267 | vals = (sl.stop for sl in slice_tuple) 268 | 269 | return cls(zip(keys, vals)) 270 | 271 | 272 | @six.add_metaclass(DictMeta) 273 | class Dict(SeqMixin, dict): 274 | 275 | def __iter__(self): 276 | """ 277 | Iterates a dict by (key, val) pairs. 278 | """ 279 | if six.PY2: 280 | return iter(self.iteritems()) 281 | 282 | if six.PY3: 283 | return iter(self.items()) 284 | 285 | 286 | # Short aliases 287 | L = List 288 | T = Tuple 289 | S = Set 290 | D = Dict 291 | -------------------------------------------------------------------------------- /f/function.py: -------------------------------------------------------------------------------- 1 | 2 | from functools import partial, wraps 3 | 4 | import six 5 | 6 | __all__ = ( 7 | 'pcall', 8 | 'pcall_wraps', 9 | 'achain', 10 | 'ichain', 11 | 'arr1', 12 | 'arr2', 13 | 'comp', 14 | 'every_pred', 15 | 'transduce', 16 | 'nth', 17 | 'first', 18 | 'second', 19 | 'third', 20 | ) 21 | 22 | reduce = six.moves.reduce 23 | range = six.moves.range 24 | 25 | 26 | def pcall(func, *args, **kwargs): 27 | """ 28 | Calls a passed function handling any exceptions. 29 | 30 | Returns either (None, result) or (exc, None) tuple. 31 | """ 32 | try: 33 | return None, func(*args, **kwargs) 34 | except Exception as e: 35 | return e, None 36 | 37 | 38 | def pcall_wraps(func): 39 | """ 40 | A decorator that wraps a function with `pcall` logic. 41 | """ 42 | @wraps(func) 43 | def wrapper(*args, **kwargs): 44 | return pcall(func, *args, **kwargs) 45 | 46 | return wrapper 47 | 48 | 49 | def achain(obj, *attrs): 50 | """ 51 | Gets through a chain of attributes 52 | handling AttributeError with None value. 53 | """ 54 | def get_attr(obj, attr): 55 | return getattr(obj, attr, None) 56 | 57 | return reduce(get_attr, attrs, obj) 58 | 59 | 60 | def ichain(obj, *items): 61 | """ 62 | Gets through a chain of items 63 | handling exceptions with None value. 64 | 65 | Useful for data restored from a JSON string: 66 | >>> ichain(data, 'result', 'users', 0, 'address', 'street') 67 | """ 68 | 69 | def get_item(obj, item): 70 | if obj is None: 71 | return None 72 | try: 73 | return obj[item] 74 | except: 75 | return None 76 | 77 | return reduce(get_item, items, obj) 78 | 79 | 80 | def arr1(value, *forms): 81 | """ 82 | Clojure's first threading macro implementation. 83 | 84 | Passes a value through the forms. Each form is either 85 | `func` or `(func, arg2, arg2, ...)`. 86 | 87 | The macro puts a value in the first form as the first argument. 88 | The result is put into the second form as the first argument, 89 | and so on. 90 | 91 | See https://clojuredocs.org/clojure.core/-> 92 | 93 | :param value: Initial value to process. 94 | :type value: any 95 | 96 | :param forms: A tuple of forms. 97 | :type forms: tuple of func|(func, arg2, arg3, ...) 98 | 99 | :return: A value passed through the all forms. 100 | :rtype: any 101 | 102 | """ 103 | def reducer(value, form): 104 | 105 | if isinstance(form, (tuple, list)): 106 | func, args = form[0], form[1:] 107 | else: 108 | func, args = form, () 109 | 110 | all_args = (value, ) + tuple(args) 111 | return func(*all_args) 112 | 113 | return reduce(reducer, forms, value) 114 | 115 | 116 | def arr2(value, *forms): 117 | """ 118 | Clojure's second threading macro implementation. 119 | 120 | The logic is the same as `thread_first`, but puts the value 121 | at the end of each form. 122 | 123 | See https://clojuredocs.org/clojure.core/->> 124 | 125 | :param value: Initial value to process. 126 | :type value: any 127 | 128 | :param forms: A tuple of forms. 129 | :type forms: tuple of func|(func, arg1, arg2, ...) 130 | 131 | :return: A value passed through the all forms. 132 | :rtype: any 133 | 134 | """ 135 | 136 | def reducer(value, form): 137 | 138 | if isinstance(form, (tuple, list)): 139 | func, args = form[0], form[1:] 140 | else: 141 | func, args = form, () 142 | 143 | all_args = tuple(args) + (value, ) 144 | return func(*all_args) 145 | 146 | return reduce(reducer, forms, value) 147 | 148 | 149 | def comp(*funcs): 150 | """ 151 | Makes a composition of passed functions: 152 | >>> comp(f, g, h)(x) <==> h(g(f(x))) 153 | """ 154 | def composed(value): 155 | return arr1(value, *funcs) 156 | 157 | names = (func.__name__ for func in funcs) 158 | composed.__name__ = "composed(%s)" % ", ".join(names) 159 | 160 | return composed 161 | 162 | 163 | def every_pred(*preds): 164 | """ 165 | Makes a super-predicate from the passed ones. 166 | A super-predicate is true only if all the child predicates 167 | are true. The evaluation is lazy (like bool expressions). 168 | """ 169 | def composed(val): 170 | for pred in preds: 171 | if not pred(val): 172 | return False 173 | return True 174 | 175 | names = (pred.__name__ for pred in preds) 176 | composed.__name__ = "predicate(%s)" % ", ".join(names) 177 | 178 | return composed 179 | 180 | 181 | def transduce(mfunc, rfunc, coll, init): 182 | """ 183 | A naive try to implement Clojure's transducers. 184 | 185 | See http://clojure.org/reference/transducers 186 | 187 | :param mfunc: A map function to apply to each element. 188 | :type mfunc: function 189 | 190 | :param rfunc: A reduce function to reduce the result after map. 191 | :type rfunc: function 192 | 193 | :param coll: A collection to process. 194 | :type coll: list|tuple|set|dict 195 | 196 | :param init: An initial element for reducing function. 197 | :type init: any 198 | 199 | :return: coll ==> map ==> reduce 200 | :rtype: any 201 | 202 | """ 203 | def reducer(result, item): 204 | return rfunc(result, mfunc(item)) 205 | 206 | return reduce(reducer, coll, init) 207 | 208 | 209 | def nth(n, coll): 210 | """ 211 | Returns an Nth element of a passed collection. 212 | Supports iterators without `__getitem__` method. 213 | Returns None if no item can be gotten. 214 | """ 215 | 216 | # Try to get by index first. 217 | if hasattr(coll, '__getitem__'): 218 | try: 219 | return coll[n] 220 | except IndexError: 221 | return None 222 | 223 | # Otherwise, try to iterate manually. 224 | elif hasattr(coll, '__iter__'): 225 | 226 | iterator = iter(coll) 227 | for x in range(n + 1): 228 | try: 229 | val = next(iterator) 230 | except StopIteration: 231 | return None 232 | return val 233 | 234 | else: 235 | return None 236 | 237 | 238 | # Aliases 239 | first = partial(nth, 0) 240 | second = partial(nth, 1) 241 | third = partial(nth, 2) 242 | -------------------------------------------------------------------------------- /f/generic.py: -------------------------------------------------------------------------------- 1 | 2 | __all__ = ( 3 | 'Generic', 4 | ) 5 | 6 | 7 | class Generic(object): 8 | """ 9 | Generic implementation. 10 | 11 | Usage: 12 | 13 | gen = f.Generic() 14 | 15 | @gen.extend(f.p_int, f.p_str) 16 | def handler1(x, y): 17 | return str(x) + y 18 | 19 | @gen.extend(f.p_int, f.p_int) 20 | def handler2(x, y): 21 | return x + y 22 | 23 | @gen.default 24 | def default_handler(*args): 25 | return "default" 26 | 27 | gen(1, "2") 28 | >>> "12" 29 | 30 | gen(1, 2, 3, 4) 31 | >>> "default" 32 | 33 | """ 34 | 35 | __slots__ = ("__funcs", "__default", ) 36 | 37 | def __init__(self): 38 | self.__funcs = {} 39 | self.__default = None 40 | 41 | def extend(self, *preds): 42 | """ 43 | Extends the generic with a handler function. 44 | 45 | :param preds: A tuple of predicates for each argument. 46 | :type preds: tuple of func(x) -> bool 47 | 48 | :return: This generic 49 | :rtype: f.Generic 50 | 51 | """ 52 | 53 | def wrapper(func): 54 | self.__funcs[preds] = func 55 | return self 56 | 57 | return wrapper 58 | 59 | def default(self, func): 60 | """ 61 | Registers the default handler for generic. 62 | 63 | The function should deal with any kind of arguments. 64 | 65 | :param func: A function default logic. 66 | :type func: function 67 | 68 | :return: This generic 69 | :rtype: f.Generic 70 | 71 | """ 72 | 73 | self.__default = func 74 | return self 75 | 76 | def __call__(self, *args): 77 | 78 | for preds in self.__funcs: 79 | if len(preds) == len(args): 80 | pairs = zip(preds, args) 81 | if all(func(arg) for (func, arg) in pairs): 82 | return self.__funcs[preds](*args) 83 | 84 | if self.__default: 85 | return self.__default(*args) 86 | else: 87 | msg = "No function was found for such arguments: %s" % args 88 | raise TypeError(msg) 89 | -------------------------------------------------------------------------------- /f/monad.py: -------------------------------------------------------------------------------- 1 | 2 | from functools import wraps 3 | 4 | __all__ = ( 5 | 'Just', 6 | 'Nothing', 7 | 'Left', 8 | 'Right', 9 | 'Success', 10 | 'Failture', 11 | 'IO', 12 | 'maybe', 13 | 'maybe_wraps', 14 | 'either', 15 | 'either_wraps', 16 | 'error', 17 | 'error_wraps', 18 | 'io', 19 | 'io_wraps', 20 | ) 21 | 22 | 23 | class Monad(object): 24 | 25 | __slots__ = ('_val', ) 26 | 27 | def __init__(self, val): 28 | self._val = val 29 | 30 | def __rshift__(self, form): 31 | 32 | if isinstance(form, (list, tuple)): 33 | func, args = form[0], form[1:] 34 | else: 35 | func = form 36 | args = () 37 | 38 | return func(self._val, *args) 39 | 40 | def bind(self, *form): 41 | return self >> form 42 | 43 | def __eq__(self, other): 44 | """ 45 | Compares two monadic values by their type and content. 46 | """ 47 | return ( 48 | type(self) == type(other) 49 | and self._val == other._val 50 | ) 51 | 52 | def __repr__(self): 53 | return "%s[%s]" % (self.__class__.__name__, self._val) 54 | 55 | def get(self): 56 | return self._val 57 | 58 | 59 | class Nothing(Monad): 60 | """ 61 | Represents a negative Maybe value. 62 | """ 63 | 64 | def __init__(self): 65 | pass 66 | 67 | def __rshift__(self, func): 68 | """ 69 | Always returns current object without performing 70 | a passed function. 71 | """ 72 | return self 73 | 74 | def __eq__(self, other): 75 | return isinstance(other, Nothing) 76 | 77 | def __repr__(self): 78 | return self.__class__.__name__ 79 | 80 | def get(self): 81 | return None 82 | 83 | 84 | class Just(Monad): 85 | """ 86 | Represents a positive Maybe value. 87 | """ 88 | pass 89 | 90 | 91 | class Left(Monad): 92 | """ 93 | Represents a negative Either value. 94 | """ 95 | 96 | def __rshift__(self, func): 97 | return self 98 | 99 | 100 | class Right(Monad): 101 | """ 102 | Represents a positive Either value. 103 | """ 104 | pass 105 | 106 | 107 | class Success(Monad): 108 | """ 109 | Represents a positive Error value. 110 | """ 111 | 112 | def recover(self, exc_class, val_or_func): 113 | """ 114 | Does nothing, returning the current monadic value. 115 | """ 116 | return self 117 | 118 | 119 | class Failture(Monad): 120 | """ 121 | Represents a negative Error value. 122 | """ 123 | 124 | def __rshift__(self, func): 125 | return self 126 | 127 | def get(self): 128 | raise self._val 129 | 130 | def recover(self, exc_class, val_or_func): 131 | """ 132 | Recovers an exception. 133 | 134 | :param exc_class: An exception class or a tuple of classes to recover. 135 | :type exc_class: Exception|tuple of Exception 136 | 137 | :param val_or_func: A value or a function to get a positive result. 138 | :type val_or_func: any|function 139 | 140 | :return: Success instance if the cought exception matches `exc_class` 141 | or Failture if it does not. 142 | :rtype: Failture|Success 143 | 144 | Usage: 145 | 146 | error(lambda: 1 / 0).recover(Exception, 42).get() 147 | >>> 42 148 | 149 | """ 150 | 151 | e = self._val 152 | 153 | def is_callable(val): 154 | return hasattr(val_or_func, '__call__') 155 | 156 | def resolve(): 157 | 158 | if is_callable(val_or_func): 159 | return val_or_func(e) 160 | 161 | else: 162 | return val_or_func 163 | 164 | if isinstance(e, exc_class): 165 | return Success(resolve()) 166 | 167 | else: 168 | return self 169 | 170 | 171 | class IO(Monad): 172 | """ 173 | Represents IO value. 174 | """ 175 | pass 176 | 177 | 178 | def maybe(pred): 179 | """ 180 | Maybe constructor. 181 | 182 | Takes a predicate and returns a function 183 | that receives `x` and returns a Maybe instance of `x`. 184 | 185 | :param pred: a function that determines if a value is Just or Nothing. 186 | :type pred: function 187 | 188 | :return: Maybe unit function. 189 | :rtype: function 190 | 191 | """ 192 | def maybe_unit(x): 193 | """ 194 | Maybe unit. 195 | 196 | :param x: Any non-monadic value. 197 | :type x: any 198 | 199 | :return: Monadic value. 200 | :rtype: Just|Nothing 201 | 202 | """ 203 | if pred(x): 204 | return Just(x) 205 | 206 | else: 207 | return Nothing() 208 | 209 | return maybe_unit 210 | 211 | 212 | def maybe_wraps(pred): 213 | """ 214 | Decorator that wraps a function with maybe behaviour. 215 | """ 216 | def decorator(func): 217 | 218 | @wraps(func) 219 | def wrapper(*args, **kwargs): 220 | return maybe(pred)(func(*args, **kwargs)) 221 | 222 | return wrapper 223 | 224 | return decorator 225 | 226 | 227 | def either(pred_l, pred_r): 228 | """ 229 | Either constructor. 230 | 231 | Takes two predicates and returns a function 232 | that receives `x` and returns an Either instance of `x`. 233 | 234 | :param pred_l: Left predicate. 235 | :type pred_l: function 236 | 237 | :param pred_r: Right predicate. 238 | :type pred_r: function 239 | 240 | :return: Either unit function. 241 | :rtype: function 242 | 243 | """ 244 | 245 | def either_unit(x): 246 | """ 247 | Either unit. 248 | 249 | :param x: Any non-monadic value. 250 | :type x: any 251 | 252 | :return: Monadic value. 253 | :rtype: Left|Right 254 | 255 | """ 256 | 257 | if pred_l(x): 258 | return Left(x) 259 | 260 | if pred_r(x): 261 | return Right(x) 262 | 263 | msg = "Value %s doesn't fit %s nor %s predicates." 264 | params = (x, pred_l, pred_r) 265 | 266 | raise TypeError(msg % params) 267 | 268 | return either_unit 269 | 270 | 271 | def either_wraps(pred_l, pred_r): 272 | """ 273 | Decorator that wraps a function with either behaviour. 274 | """ 275 | 276 | def decorator(func): 277 | 278 | @wraps(func) 279 | def wrapper(*args, **kwargs): 280 | return either(pred_l, pred_r)(func(*args, **kwargs)) 281 | 282 | return wrapper 283 | 284 | return decorator 285 | 286 | 287 | def error(func): 288 | """ 289 | Error constructor. 290 | 291 | Takes a function with additional arguments. Calls the function 292 | handling any possible exceptions. Returns either `Success` 293 | with actual value or `Failture` with an exception instance inside. 294 | 295 | :param func: A function to call. 296 | :type func: function 297 | 298 | :param args: A tuple of positional parameters 299 | :type args: tuple of any 300 | 301 | :param kwargs: A dict of named parameters. 302 | :type kwargs: dict of any 303 | 304 | :return: Monadic value of Error 305 | :rtype: Success|Failture 306 | 307 | """ 308 | 309 | def error_unit(*args, **kwargs): 310 | try: 311 | return Success(func(*args, **kwargs)) 312 | except Exception as e: 313 | return Failture(e) 314 | 315 | return error_unit 316 | 317 | 318 | error_wraps = error 319 | 320 | 321 | def io(func): 322 | """ 323 | Decorator that wraps a function with IO behaviour. 324 | """ 325 | 326 | @wraps(func) 327 | def wrapper(*args, **kwargs): 328 | return IO(func(*args, **kwargs)) 329 | 330 | return wrapper 331 | 332 | 333 | io_wraps = io 334 | -------------------------------------------------------------------------------- /f/predicate.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import operator 4 | from functools import partial, wraps 5 | 6 | import six 7 | 8 | __all__ = ( 9 | "p_gt", 10 | "p_gte", 11 | "p_lt", 12 | "p_lte", 13 | "p_eq", 14 | "p_not_eq", 15 | "p_in", 16 | "p_is", 17 | "p_is_not", 18 | "p_and", 19 | "p_or", 20 | "p_inst", 21 | 22 | "p_str", 23 | "p_int", 24 | "p_float", 25 | "p_num", 26 | "p_list", 27 | "p_tuple", 28 | "p_set", 29 | "p_dict", 30 | "p_array", 31 | "p_coll", 32 | "p_truth", 33 | "p_none", 34 | "p_not_none", 35 | "p_even", 36 | "p_odd", 37 | ) 38 | 39 | # 40 | # Binary 41 | # 42 | 43 | 44 | def _binary(func, b): 45 | """ 46 | A basic form of binary predicate. 47 | 48 | `func` is a two-argument function. 49 | `b` is a second argument. 50 | 51 | Returns a function that takes the first argument 52 | and returns a bool value. 53 | """ 54 | @wraps(func) 55 | def wrapper(a): 56 | return func(a, b) 57 | 58 | return wrapper 59 | 60 | 61 | p_gt = partial(_binary, operator.gt) 62 | p_gte = partial(_binary, operator.ge) 63 | p_lt = partial(_binary, operator.lt) 64 | p_lte = partial(_binary, operator.le) 65 | p_eq = partial(_binary, operator.eq) 66 | p_not_eq = partial(_binary, (lambda a, b: a != b)) 67 | p_in = partial(_binary, (lambda x, coll: x in coll)) 68 | p_is = partial(_binary, operator.is_) 69 | p_is_not = partial(_binary, operator.is_not) 70 | p_and = partial(_binary, (lambda a, b: a and b)) 71 | p_or = partial(_binary, (lambda a, b: a or b)) 72 | p_inst = partial(_binary, isinstance) 73 | 74 | # 75 | # Unary 76 | # 77 | 78 | p_str = p_inst(six.string_types) 79 | p_int = p_inst(six.integer_types) 80 | p_float = p_inst(float) 81 | p_num = p_inst(six.integer_types + (float, )) 82 | p_list = p_inst(list) 83 | p_tuple = p_inst(tuple) 84 | p_set = p_inst(set) 85 | p_dict = p_inst(dict) 86 | p_array = p_inst((list, tuple)) 87 | p_coll = p_inst((list, tuple, dict, set)) 88 | 89 | 90 | def p_truth(x): 91 | return bool(x) is True 92 | 93 | 94 | def p_none(x): 95 | return x is None 96 | 97 | 98 | def p_not_none(x): 99 | return x is not None 100 | 101 | 102 | def p_even(x): 103 | return x % 2 == 0 104 | 105 | 106 | def p_odd(x): 107 | return x % 2 != 0 108 | -------------------------------------------------------------------------------- /pip.requirements: -------------------------------------------------------------------------------- 1 | six==1.10.0 2 | -------------------------------------------------------------------------------- /pip.requirements.dev: -------------------------------------------------------------------------------- 1 | -r pip.requirements.test 2 | flake8==2.6.2 3 | ipython==5.0.0 4 | ipdb==0.10.1 5 | -------------------------------------------------------------------------------- /pip.requirements.test: -------------------------------------------------------------------------------- 1 | -r pip.requirements 2 | pytest==2.9.2 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [wheel] 5 | universal = 1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import f 4 | 5 | try: 6 | from setuptools import setup 7 | except ImportError: 8 | from distutils.core import setup 9 | 10 | 11 | setup( 12 | name='f', 13 | version=".".join(map(str, f.__version__)), 14 | description="Functional tools, collections and monads.", 15 | author=f.__author__, 16 | author_email=f.__email__, 17 | url="https://github.com/igrishaev/f", 18 | packages=['f'], 19 | include_package_data=True, 20 | install_requires=[ 21 | 'Six', 22 | ], 23 | license=open('LICENSE').read(), 24 | zip_safe=False, 25 | classifiers=( 26 | 'Development Status :: 2 - Pre-Alpha', 27 | 'Intended Audience :: Developers', 28 | 'Natural Language :: English', 29 | 'License :: OSI Approved :: MIT License', 30 | 'Programming Language :: Python', 31 | 'Programming Language :: Python :: 2.6', 32 | 'Programming Language :: Python :: 2.7', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: 3.1', 35 | 'Programming Language :: Python :: 3.2', 36 | 'Programming Language :: Python :: 3.3', 37 | 'Programming Language :: Python :: 3.4', 38 | 'Programming Language :: Python :: 3.5', 39 | ), 40 | ) 41 | -------------------------------------------------------------------------------- /tests/test_collection.py: -------------------------------------------------------------------------------- 1 | 2 | import f 3 | 4 | import six 5 | 6 | 7 | def test_list(): 8 | 9 | l1 = f.L[1, 2, 3] 10 | assert l1 == [1, 2, 3] 11 | 12 | l2 = l1.map(str) 13 | assert l2 == ["1", "2", "3"] 14 | assert isinstance(l2, f.L) 15 | assert l1 == [1, 2, 3] 16 | 17 | assert l1.map(str).join("-") == "1-2-3" 18 | 19 | result = [] 20 | 21 | def collect(x, delta=0): 22 | result.append(x + delta) 23 | 24 | l1.foreach(collect, delta=1) 25 | assert result == [2, 3, 4] 26 | 27 | def inc(x, delta=1): 28 | return x + delta 29 | 30 | l2 = l1.map(inc, delta=2) 31 | assert l2 == [3, 4, 5] 32 | assert isinstance(l2, f.L) 33 | 34 | l2 = l1.filter(f.p_even) 35 | assert l2 == [2] 36 | assert isinstance(l2, f.L) 37 | 38 | assert l1.reduce(1, (lambda a, b: a + b)) == 7 39 | assert f.L[1, 2, 3].reduce((), (lambda res, x: res + (x, ))) == (1, 2, 3) 40 | 41 | assert l1.sum() == 6 42 | 43 | def summer(*nums): 44 | return sum(nums) 45 | 46 | assert l1.apply(summer) == 6 47 | 48 | res = l1 + (1, 2, 3) 49 | assert res == [1, 2, 3, 1, 2, 3] 50 | assert isinstance(res, f.L) 51 | 52 | assert six.text_type(l1) == "List[1, 2, 3]" 53 | 54 | t1 = l1.T() 55 | assert t1 == (1, 2, 3) 56 | assert isinstance(t1, f.Tuple) 57 | 58 | l2 = l1.group(2) 59 | assert l2 == [[1, 2], [3]] 60 | assert isinstance(l2, f.L) 61 | 62 | el1, el2 = l2 63 | assert isinstance(el1, f.L) 64 | assert isinstance(el2, f.L) 65 | 66 | d1 = f.L["a", 1, "b", 2].group(2).D() 67 | assert d1 == {"a": 1, "b": 2} 68 | assert isinstance(d1, f.D) 69 | 70 | res = f.L[3, 3, 2, 1, 1, 3, 2, 2, 3].distinct() 71 | assert [3, 2, 1] == res 72 | assert isinstance(res, f.L) 73 | 74 | res = f.L[{1: 2}, {1: 2}, {1: 3}].distinct() 75 | assert [{1: 2}, {1: 3}] == res 76 | assert isinstance(res, f.L) 77 | 78 | res = f.L[3, 2, 1, 1, 2, 3].sorted() 79 | assert [1, 1, 2, 2, 3, 3] == res 80 | assert isinstance(res, f.L) 81 | 82 | res = f.L[(3, 1), (2, 2), (1, 3)].sorted(key=(lambda pair: pair[0])) 83 | assert [(1, 3), (2, 2), (3, 1)] == res 84 | assert isinstance(res, f.L) 85 | 86 | 87 | def test_list_constructor(): 88 | 89 | l = f.L((1, 2, 3)) 90 | assert [1, 2, 3] == l 91 | 92 | l = f.L[1, 2, 3] 93 | assert [1, 2, 3] == l 94 | 95 | l = f.L[1] 96 | assert [1] == l 97 | 98 | 99 | def test_tuple_constructor(): 100 | 101 | t = f.T([1, 2, 3]) 102 | assert (1, 2, 3) == t 103 | assert isinstance(t, f.T) 104 | 105 | t = f.T[1, 2, 3] 106 | assert (1, 2, 3) == t 107 | assert isinstance(t, f.T) 108 | 109 | t = f.T[1] 110 | assert (1, ) == t 111 | assert isinstance(t, f.T) 112 | 113 | 114 | def test_set_constructor(): 115 | 116 | s = f.S((1, 2, 3)) 117 | assert set((1, 2, 3)) == s 118 | assert isinstance(s, f.S) 119 | 120 | s = f.S[1, 2, 3] 121 | assert set((1, 2, 3)) == s 122 | assert isinstance(s, f.S) 123 | 124 | s = f.S[1] 125 | assert set((1, )) == s 126 | assert isinstance(s, f.S) 127 | 128 | 129 | def test_list_slice(): 130 | 131 | res = f.L[1, 2, 3][:1] 132 | assert [1] == res 133 | assert isinstance(res, f.L) 134 | 135 | 136 | def test_tuple_slice(): 137 | 138 | res = f.T[1, 2, 3][:1] 139 | assert (1, ) == res 140 | assert isinstance(res, f.T) 141 | 142 | 143 | def test_set_features(): 144 | 145 | s = f.S[1, 2, 3] 146 | 147 | assert set(("1", "2", "3")) == s.map(str) 148 | 149 | assert f.S["a", "b"].join("-") in ("a-b", "b-a") 150 | 151 | res = s.filter(f.p_even) 152 | assert set((2, )) == res 153 | assert isinstance(res, f.S) 154 | 155 | assert 106 == s.reduce(100, (lambda res, x: res + x)) 156 | 157 | assert 6 == s.sum() 158 | 159 | res = s + ["a", 1, "b", 3, "c"] 160 | assert set((1, 2, 3, "a", "b", "c")) == res 161 | assert isinstance(res, f.S) 162 | 163 | assert set((1, 2, 3)) == s 164 | 165 | 166 | def test_dict_features(): 167 | 168 | d = f.D(a=1, b=2, c=3) 169 | res = d + dict(d=4, e=5, f=5) 170 | 171 | assert dict(a=1, b=2, c=3) == d 172 | assert res == dict(a=1, b=2, c=3, d=4, e=5, f=5) 173 | assert isinstance(res, f.D) 174 | 175 | def my_filter(pair): 176 | key, val = pair 177 | return key == 1 and val == 1 178 | 179 | d = f.D[2: 3, 3: 1, 1: 1] 180 | d2 = d.filter(my_filter) 181 | assert d2 == {1: 1} 182 | assert isinstance(d2, f.D) 183 | 184 | 185 | def test_dict_constructor(): 186 | 187 | d = f.D["foo": 42] 188 | assert dict(foo=42) == d 189 | 190 | d = f.D["name": "Juan", "sex": "male", 1: 42, None: [1, 2, 3]] 191 | assert {"name": "Juan", "sex": "male", 1: 42, None: [1, 2, 3]} == d 192 | assert isinstance(d, f.D) 193 | 194 | d = f.D["foo": f.D["bar": f.D["baz": 42]]] 195 | node = d["foo"]["bar"] 196 | assert isinstance(node, f.D) 197 | assert 42 == node["baz"] 198 | 199 | assert "Dict{'foo': Dict{'bar': Dict{'baz': 42}}}" == six.text_type(d) 200 | 201 | 202 | def test_dict_iter(): 203 | assert list(f.D[1: 2]) == [(1, 2)] 204 | 205 | 206 | def test_complex(): 207 | origin = f.L[4, 3, 2, 1] 208 | 209 | def pred(pair): 210 | k, v = pair 211 | return k == "1" and v == "2" 212 | 213 | res = origin.map(str).Tuple().reversed() \ 214 | .group(2).Dict().filter(pred) 215 | 216 | assert res == {"1": "2"} 217 | assert isinstance(res, f.D) 218 | 219 | assert origin == [4, 3, 2, 1] 220 | 221 | assert "99-98-97" == f.L("abc").map(ord).map(str).reversed().join("-") 222 | 223 | assert f.L[1, 2, 3].map(str).Tuple() == f.T["1", "2", "3"] 224 | assert f.L[1, 2, 3][:-1].map(str) == f.L["1", "2"] 225 | 226 | assert set(f.D(bar=2, foo=1)) == set((("foo", 1), ("bar", 2))) 227 | 228 | 229 | def test_repr(): 230 | assert "List[1, 2, 3]" == repr(f.L[1, 2, 3]) 231 | assert "Tuple(1, 2, 3)" == repr(f.T[1, 2, 3]) 232 | assert "Dict{1: 2}" == repr(f.D[1: 2]) 233 | assert "Set{1}" == repr(f.S[1]) 234 | -------------------------------------------------------------------------------- /tests/test_function.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | 4 | import f 5 | 6 | 7 | # 8 | # helpers 9 | # 10 | 11 | 12 | def json_data(): 13 | return { 14 | "result": [ 15 | {"name": "Ivan", "kids": [ 16 | {"name": "Leo", "age": 7}, 17 | {"name": "Ann", "age": 1}, 18 | ]}, 19 | {"name": "Juan", "kids": None}, 20 | ] 21 | } 22 | 23 | 24 | def double(a): 25 | return a * 2 26 | 27 | 28 | def inc(a): 29 | return a + 1 30 | 31 | 32 | def plus(a, b): 33 | """ 34 | Adds two numbers. 35 | """ 36 | return a + b 37 | 38 | 39 | def div(a, b): 40 | return a / b 41 | 42 | 43 | class Rabbit: 44 | class Duck: 45 | class Egg: 46 | class Needle: 47 | class Death: 48 | on = True 49 | 50 | 51 | # 52 | # tests 53 | # 54 | 55 | 56 | def test_pcall_ok(): 57 | assert (None, 3) == f.pcall(plus, 1, 2) 58 | 59 | 60 | def test_pcall_err(): 61 | err, result = f.pcall(div, 1, 0) 62 | assert result is None 63 | assert isinstance(err, ZeroDivisionError) 64 | 65 | 66 | def test_pcall_arity_err(): 67 | err, result = f.pcall(plus, 1, 2, 1) 68 | assert result is None 69 | assert isinstance(err, TypeError) 70 | 71 | 72 | def test_pcall_wraps(): 73 | 74 | safe_div = f.pcall_wraps(div) 75 | assert (None, 2) == safe_div(10, 5) 76 | 77 | err, result = safe_div(10, 0) 78 | assert result is None 79 | assert isinstance(err, ZeroDivisionError) 80 | 81 | 82 | def test_pcall_wraps_name(): 83 | safe_div = f.pcall_wraps(div) 84 | assert safe_div.__name__ == div.__name__ 85 | 86 | 87 | def test_achain(): 88 | assert True is f.achain( 89 | Rabbit, 'Duck', 'Egg', 'Needle', 'Death', 'on') 90 | 91 | 92 | def test_achain_missed(): 93 | assert None is f.achain( 94 | Rabbit, 'Duck', 'Egg', 'Needle', 'Life', 'on') 95 | 96 | 97 | def test_ichain_ok(): 98 | data = json_data() 99 | assert 7 == f.ichain(data, 'result', 0, 'kids', 0, 'age') 100 | 101 | 102 | def test_ichain_missing(): 103 | data = json_data() 104 | assert f.ichain(data, 'foo', 'bar', 999, None) is None 105 | 106 | 107 | def test_arr1(): 108 | assert "__" == f.arr1( 109 | -42, 110 | (plus, 2), 111 | abs, 112 | str, 113 | (str.replace, "40", "__") 114 | ) 115 | 116 | 117 | def test_arr2(): 118 | result = f.arr2( 119 | -2, 120 | abs, 121 | (plus, 2), 122 | str, 123 | ("000".replace, "0") 124 | ) 125 | assert "444" == result 126 | 127 | 128 | def test_comp(): 129 | comp = f.comp(abs, double, str) 130 | assert "84" == comp(-42) 131 | 132 | 133 | def test_comp_name(): 134 | comp = f.comp(abs, double, str) 135 | assert "composed(abs, double, str)" in comp.__name__ 136 | 137 | 138 | def test_every_pred(): 139 | 140 | pred1 = f.p_gt(0) 141 | pred2 = f.p_even 142 | pred3 = f.p_not_eq(666) 143 | 144 | every = f.every_pred(pred1, pred2, pred3) 145 | 146 | result = filter(every, (-1, 1, -2, 2, 3, 4, 666, -3, 1, 2)) 147 | assert (2, 4, 2) == tuple(result) 148 | 149 | 150 | def test_every_pred_lazy(): 151 | 152 | def pred1(x): 153 | return False 154 | 155 | def pred2(x): 156 | raise ValueError("some error") 157 | 158 | every = f.every_pred(pred1, pred2) 159 | 160 | result = filter(every, (-1, 1, -2, 2, 3, 4, 666, -3, 1, 2)) 161 | try: 162 | assert () == tuple(result) 163 | except ValueError: 164 | pytest.fail("Should not be risen!") 165 | 166 | 167 | def test_every_pred_name(): 168 | 169 | def is_positive(x): 170 | return x > 0 171 | 172 | def is_even(x): 173 | return x % 2 == 0 174 | 175 | every = f.every_pred(is_positive, is_even) 176 | assert "predicate(is_positive, is_even)" in str(every.__name__) 177 | 178 | 179 | def test_transduce(): 180 | 181 | result = f.transduce( 182 | inc, 183 | (lambda res, item: res + str(item)), 184 | (1, 2, 3), 185 | "" 186 | ) 187 | assert "234" == result 188 | 189 | 190 | def test_transduce_comp(): 191 | 192 | result = f.transduce( 193 | f.comp(abs, inc, double), 194 | (lambda res, item: res + (item, )), 195 | [-1, -2, -3], 196 | () 197 | ) 198 | 199 | assert (4, 6, 8) == result 200 | 201 | 202 | def test_first(): 203 | assert 1 == f.first((1, 2, 3)) 204 | 205 | 206 | def test_second(): 207 | assert 2 == f.second((1, 2, 3)) 208 | 209 | 210 | def test_third(): 211 | assert 3 == f.third((1, 2, 3)) 212 | 213 | 214 | def test_nth(): 215 | assert 1 == f.nth(0, [1, 2, 3]) 216 | assert None is f.nth(9, [1, 2, 3]) 217 | 218 | 219 | def test_nth_no_index(): 220 | assert 1 == f.nth(0, set([1])) 221 | assert None is f.nth(2, set([1])) 222 | -------------------------------------------------------------------------------- /tests/test_generic.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | 4 | import f 5 | 6 | 7 | @pytest.yield_fixture 8 | def gen(): 9 | 10 | gen = f.Generic() 11 | 12 | @gen.extend(f.p_int, f.p_str) 13 | def handler1(x, y): 14 | return str(x) + y 15 | 16 | @gen.extend(f.p_int, f.p_int) 17 | def handler2(x, y): 18 | return x + y 19 | 20 | @gen.extend(f.p_str, f.p_str) 21 | def handler3(x, y): 22 | return x + y + x + y 23 | 24 | @gen.extend(f.p_str) 25 | def handler4(x): 26 | return "-".join(reversed(x)) 27 | 28 | @gen.extend() 29 | def handler5(): 30 | return 42 31 | 32 | @gen.extend(f.p_none) 33 | def handler6(x): 34 | return gen(1, 2) 35 | 36 | yield gen 37 | 38 | 39 | def test_ok(gen): 40 | 41 | assert 3 == gen(None) 42 | assert "12" == gen(1, "2") 43 | assert 3 == gen(1, 2) 44 | assert "fizbazfizbaz" == gen("fiz", "baz") 45 | assert "o-l-l-e-h" == gen("hello") 46 | assert 42 == gen() 47 | 48 | 49 | def test_no_handler(gen): 50 | 51 | with pytest.raises(TypeError): 52 | gen(1, 2, 3, 4) 53 | 54 | 55 | def test_default(gen): 56 | 57 | @gen.default 58 | def default_handler(*args): 59 | return "default" 60 | 61 | assert "default" == gen(1, 2, 3, 4) 62 | 63 | 64 | def test_handler_is_gen(gen): 65 | 66 | @gen.extend(f.p_dict) 67 | def handler8(x): 68 | return 100500 69 | 70 | assert handler8 is gen 71 | assert 100500 == handler8({}) 72 | -------------------------------------------------------------------------------- /tests/test_monad.py: -------------------------------------------------------------------------------- 1 | 2 | import math 3 | 4 | import f 5 | 6 | import pytest 7 | import six 8 | 9 | # 10 | # helpers 11 | # 12 | 13 | 14 | @f.maybe_wraps(f.p_num) 15 | def mdiv(a, b): 16 | if b: 17 | return a / b 18 | else: 19 | return None 20 | 21 | 22 | @f.maybe_wraps(f.p_num) 23 | def msqrt(a): 24 | if a >= 0: 25 | return math.sqrt(a) 26 | else: 27 | return None 28 | 29 | 30 | @f.either_wraps(f.p_str, f.p_num) 31 | def ediv(a, b): 32 | if b == 0: 33 | return "Div by zero: %s / %s" % (a, b) 34 | else: 35 | return a / b 36 | 37 | 38 | @f.either_wraps(f.p_str, f.p_num) 39 | def esqrt(a): 40 | if a < 0: 41 | return "Negative number: %s" % a 42 | else: 43 | return math.sqrt(a) 44 | 45 | 46 | @f.error_wraps 47 | def tdiv(a, b): 48 | return a / b 49 | 50 | 51 | @f.error_wraps 52 | def tsqrt(a): 53 | return math.sqrt(a) 54 | 55 | 56 | @pytest.mark.parametrize("x,val", ( 57 | (2, f.Just), 58 | (2.0, f.Just), 59 | (-2, f.Nothing), 60 | )) 61 | def test_maybe_laws(x, val): 62 | 63 | p = f.p_num 64 | unit = f.maybe(p) 65 | 66 | @f.maybe_wraps(p) 67 | def g(x): 68 | if x > 0: 69 | return x * 2 70 | else: 71 | return None 72 | 73 | @f.maybe_wraps(p) 74 | def h(x): 75 | if x > 0: 76 | return x + 2 77 | else: 78 | return None 79 | 80 | assert isinstance(g(x), val) 81 | 82 | # 1 83 | assert unit(x) >> g == g(x) 84 | 85 | # 2 86 | mv = g(x) 87 | assert mv >> unit == mv 88 | 89 | # 3 90 | (mv >> g) >> h == mv >> (lambda x: g(x) >> h) 91 | 92 | 93 | def test_maybe(): 94 | 95 | unit = f.maybe(f.p_int) 96 | 97 | m = unit(42) 98 | assert isinstance(m, f.Just) 99 | 100 | assert 42 == m.get() 101 | 102 | m = unit(None) 103 | assert isinstance(m, f.Nothing) 104 | 105 | m = mdiv(16, 4) >> msqrt 106 | assert isinstance(m, f.Just) 107 | 108 | m = mdiv(16, 4.0) >> msqrt 109 | assert isinstance(m, f.Just) 110 | assert 2 == m.get() 111 | 112 | m = mdiv(16, 0) >> msqrt 113 | assert isinstance(m, f.Nothing) 114 | assert None is m.get() 115 | 116 | m = mdiv(16, -4) >> msqrt 117 | assert isinstance(m, f.Nothing) 118 | 119 | 120 | def test_either(): 121 | 122 | unit = f.either(f.p_str, f.p_num) 123 | 124 | m = unit("error") 125 | assert isinstance(m, f.Left) 126 | # assert "error" == m.get() 127 | 128 | m = unit(42) 129 | assert isinstance(m, f.Right) 130 | # assert 42 == m.get() 131 | 132 | m = ediv(16, 4) >> esqrt 133 | assert isinstance(m, f.Right) 134 | # assert 2 == m.get() 135 | 136 | m = ediv(16, -4) >> esqrt 137 | assert isinstance(m, f.Left) 138 | # assert "Negative number: -4" == m.get() 139 | 140 | m = ediv(16, 0) >> esqrt 141 | assert isinstance(m, f.Left) 142 | # assert "Div by zero: 16 / 0" == m.get() 143 | 144 | 145 | @pytest.mark.parametrize("x,val", ( 146 | (2, f.Right), 147 | (2.0, f.Right), 148 | (-2, f.Left), 149 | )) 150 | def test_either_laws(x, val): 151 | 152 | p = (f.p_str, f.p_num) 153 | unit = f.either(*p) 154 | 155 | @f.either_wraps(*p) 156 | def g(x): 157 | if x > 0: 158 | return x + 2 159 | else: 160 | return "less the zero" 161 | 162 | @f.either_wraps(*p) 163 | def h(x): 164 | if x > 0: 165 | return x + 3 166 | else: 167 | return "less the zero2" 168 | 169 | assert isinstance(g(x), val) 170 | 171 | # 1 172 | assert unit(x) >> g == g(x) 173 | 174 | # 2 175 | mv = g(x) 176 | assert mv >> unit == mv 177 | 178 | # 3 179 | (mv >> g) >> h == mv >> (lambda x: g(x) >> h) 180 | 181 | 182 | def test_error(): 183 | 184 | unit = f.error(lambda a, b: a / b) 185 | 186 | m = unit(16, b=4) 187 | assert isinstance(m, f.Success) 188 | 189 | assert 4 == m.get() 190 | 191 | m = unit(16, b=0) 192 | assert isinstance(m, f.Failture) 193 | 194 | with pytest.raises(ZeroDivisionError): 195 | m.get() 196 | 197 | 198 | def test_failture(): 199 | 200 | unit = f.error(lambda: 1 / 0) 201 | 202 | m = unit() 203 | res = m.recover(ZeroDivisionError, 0) 204 | assert isinstance(res, f.Success) 205 | 206 | assert 0 == res.get() 207 | 208 | with pytest.raises(ZeroDivisionError): 209 | m.get() 210 | 211 | res = m.recover(MemoryError, 0) 212 | assert isinstance(res, f.Failture) 213 | 214 | 215 | def test_failture_recover_multi(): 216 | 217 | unit = f.error(lambda: 1 / 0) 218 | 219 | m = unit() 220 | res = m \ 221 | .recover(MemoryError, 1) \ 222 | .recover(TypeError, 2) \ 223 | .recover(ValueError, 3) 224 | 225 | assert isinstance(res, f.Failture) 226 | 227 | def handler(exc): 228 | return exc.__class__.__name__ 229 | 230 | res2 = res.recover((AttributeError, ZeroDivisionError), handler) 231 | assert isinstance(res2, f.Success) 232 | assert "ZeroDivisionError" == res2.get() 233 | 234 | 235 | def test_success_recover(): 236 | 237 | unit = f.error(lambda: 1) 238 | m = unit().recover(Exception, 0) 239 | assert isinstance(m, f.Success) 240 | assert 1 == m.get() 241 | 242 | 243 | def test_try_decorator(): 244 | 245 | m = tdiv(16, 4) >> tsqrt 246 | assert isinstance(m, f.Success) 247 | assert 2 == m.get() 248 | 249 | m = tdiv(16, 0) >> tsqrt 250 | assert isinstance(m, f.Failture) 251 | with pytest.raises(ZeroDivisionError): 252 | m.get() 253 | 254 | m = tdiv(16, -4) >> tsqrt 255 | assert isinstance(m, f.Failture) 256 | with pytest.raises(ValueError): 257 | m.get() 258 | 259 | 260 | def test_io(monkeypatch, capsys): 261 | 262 | if six.PY2: 263 | path = '__builtin__.raw_input' 264 | 265 | if six.PY3: 266 | path = 'builtins.input' 267 | 268 | monkeypatch.setattr(path, lambda prompt: "hello") 269 | 270 | @f.io_wraps 271 | def read_line(prompt): 272 | if six.PY2: 273 | return raw_input("say: ") 274 | if six.PY3: 275 | return input("say: ") 276 | 277 | @f.io_wraps 278 | def write_line(text): 279 | six.print_(text) 280 | 281 | res = read_line("test: ") >> write_line 282 | assert isinstance(res, f.IO) 283 | assert None is res.get() 284 | 285 | out, err = capsys.readouterr() 286 | assert "hello\n" == out 287 | assert "" == err 288 | 289 | 290 | def test_maybe_unit(): 291 | 292 | Maybe = f.maybe(f.p_int) 293 | 294 | m = Maybe(42) 295 | assert isinstance(m, f.Just) 296 | assert 42 == m.get() 297 | 298 | m = Maybe("error") 299 | assert isinstance(m, f.Nothing) 300 | assert None is m.get() 301 | 302 | 303 | def test_either_unit(): 304 | 305 | Either = f.either(f.p_str, f.p_num) 306 | 307 | m = Either(42) 308 | assert isinstance(m, f.Right) 309 | assert 42 == m.get() 310 | 311 | m = Either("error") 312 | assert isinstance(m, f.Left) 313 | assert "error" == m.get() 314 | 315 | with pytest.raises(TypeError): 316 | Either(None) 317 | 318 | 319 | def test_monad_binding(): 320 | 321 | unit = f.maybe(f.p_int) 322 | 323 | @f.maybe_wraps(f.p_int) 324 | def g(x, y, z): 325 | return x + y + z 326 | 327 | assert f.Just(28) == unit(1) >> (g, 2, 3) >> (g, 4, 5) >> (g, 6, 7) 328 | assert f.Just(28) == unit(1).bind(g, 2, 3) \ 329 | .bind(g, 4, 5).bind(g, 6, 7) 330 | 331 | def h(x): 332 | return f.Nothing() 333 | 334 | assert f.Nothing() == unit(1) >> h 335 | assert f.Nothing() == unit(None).bind(f) 336 | -------------------------------------------------------------------------------- /tests/test_predicate.py: -------------------------------------------------------------------------------- 1 | 2 | import f 3 | 4 | 5 | def test_unary(): 6 | 7 | assert f.p_str("test") 8 | assert f.p_str(0) is False 9 | assert f.p_str(u"test") is True 10 | 11 | assert f.p_num(1) 12 | assert f.p_num(1.0) 13 | 14 | assert f.p_array([1, 2, 3]) 15 | assert f.p_array((1, 2, 3)) 16 | 17 | assert f.p_truth(1) 18 | assert f.p_truth(None) is False 19 | assert f.p_truth([]) is False 20 | 21 | assert f.p_none(None) 22 | assert f.p_none(42) is False 23 | 24 | 25 | def test_binary(): 26 | 27 | p = f.p_gt(0) 28 | assert p(1) 29 | assert p(100) 30 | assert p(0) is False 31 | assert p(-1) is False 32 | 33 | p = f.p_gte(0) 34 | assert p(0) 35 | assert p(1) 36 | assert p(-1) is False 37 | 38 | p = f.p_eq(42) 39 | assert p(42) 40 | assert p(False) is False 41 | 42 | p = f.p_not_eq(42) 43 | assert p(42) is False 44 | assert p(False) 45 | 46 | ob1 = object() 47 | p = f.p_is(ob1) 48 | assert p(object()) is False 49 | assert p(ob1) 50 | 51 | p = f.p_in((1, 2, 3)) 52 | assert p(1) 53 | assert p(3) 54 | assert p(4) is False 55 | 56 | p = f.p_and(False) 57 | assert p(True) is False 58 | assert p(False) is False 59 | 60 | p = f.p_or(42) 61 | assert p(0) == 42 62 | assert p(1) == 1 63 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py35 3 | 4 | [testenv] 5 | deps = -rpip.requirements.test 6 | commands = py.test -s -x -v 7 | --------------------------------------------------------------------------------