Ненормальное функциональное программирование на python


    После просмотра курса Programming Languages и прочтения Functional JavaScript захотелось повторить все эти крутые штуки в python. Часть вещей получилось сделать красиво и легко, остальное вышло страшным и непригодным для использования.

    Статья включает в себя:
    • немного непонятных слов;
    • каррирование;
    • pattern matching;
    • рекурсия (включая хвостовую).


    Статья рассчитана на python 3.3+.

    Немного непонятных слов


    На python можно писать в функциональном стиле, ведь в нем есть анонимные функции:
    sum_x_y = lambda x, y: x + y
    print(sum_x_y(1, 2))  # 3
    

    Функции высшего порядка (принимающие или возвращающие другие функции):
    def call_and_twice(fnc, x, y):
        return fnc(x, y) * 2
    
    print(call_and_twice(sum_x_y, 3, 4))  # 14
    

    Замыкания:
    def closure_sum(x):
        fnc = lambda y: x + y
        return fnc
    
    sum_with_3 = closure_sum(3)
    print(sum_with_3(12))  # 15
    

    Tuple unpacking(почти pattern matching):
    a, b, c = [1, 2, 3]
    print(a, b, c)  # 1 2 3
    hd, *tl = range(5)
    print(hd, 'tl:', *tl)  # 0 tl: 1 2 3 4
    

    И крутые модули functools и itertools.

    Каррирование


    Преобразование функции от многих аргументов в функцию, берущую свои аргументы по одному.

    Рассмотрим самый простой случай, каррируем функцию sum_x_y:
    sum_x_y_carry = lambda x: lambda y: sum_x_y(x, y)
    print(sum_x_y_carry(5)(12))  # 17
    

    Что-то совсем не круто, попробуем так:
    sum_with_12 = sum_x_y_carry(12)
    print(sum_with_12(1), sum_with_12(12))  # 13 24
    sum_with_5 = sum_x_y_carry(5)
    print(sum_with_5(10), sum_with_5(17))  # 15 22
    

    Уже интересней, теперь сделаем универсальную функцию для каррирования функций с двумя аргументами, ведь каждый раз писать lambda x: lambda y: zzzz совсем не круто:
    curry_2 = lambda fn: lambda x: lambda y: fn(x, y)
    

    И применим ее к используемой в реальных проектах функции map:
    curry_map_2 = curry_2(map)
    
    @curry_map_2
    def twice_or_increase(n):
        if n % 2 == 0:
            n += 1
        if n % 3:
            n *= 2
        return n
    
    print(*twice_or_increase(range(10)))  # 2 2 3 3 10 10 14 14 9 9
    print(*twice_or_increase(range(30)))  # 2 2 3 3 10 10 14 14 9 9 22 22 26 26 15 15 34 34 38...
    

    Да-да, я использовал каррированый map как декоратор и нивелировал этим отсутствие многострочных лямбд.

    Но не все функции принимают 2 аргумента, поэтому сделаем функцию curry_n, используя partial, замыкания и немножко рекурсии:
    from functools import partial
    
    def curry_n(fn, n):
        def aux(x, n=None, args=None):  # вспомогательная функция
            args = args + [x]  # добавим аргумент в список всех аргументов
            return partial(aux, n=n - 1, args=args) if n > 1 else fn(*args) # вернем функцию с одним аргументом, созданную из aux либо вызовем изначальную с полученными аргументами
        return partial(aux, n=n, args=[])
    

    И в очередной раз применим к map, но уже с 3 аргументами:
    curry_3_map = curry_n(map, 3)
    

    И сделаем функцию для сложения элементов списка с элементами списка 1..10:
    sum_arrays = curry_3_map(lambda x, y: x + y)
    sum_with_range_10 = sum_arrays(range(10))
    print(*sum_with_range_10(range(100, 0, -10)))  # 100 91 82 73 64 55 46 37 28 19
    print(*sum_with_range_10(range(10)))  # 0 2 4 6 8 10 12 14 16 18
    

    Так как curry_2 — это частный случай curry_n, то можно сделать:
    curry_2 = partial(curry_n, n=2)
    

    И для примера применим его к filter:
    curry_filter = curry_2(filter)
    only_odd = curry_filter(lambda n: n % 2)
    print(*only_odd(range(10)))  # 1 3 5 7 9
    print(*only_odd(range(-10, 0, 1)))  # -9 -7 -5 -3 -1
    

    Pattern matching


    Метод анализа списков или других структур данных на наличие в них заданных образцов.

    Pattern matching — это то, что больше всего мне понравилось в sml и хуже всего вышло в python.
    Придумаем себе цель — написать функцию, которая:
    • если принимает список чисел, возвращает их произведение;
    • если принимает список строк, возвращает одну большую объединенную строку.

    Создадим вспомогательный exception и функцию для его «бросания», которую будем использовать, когда сопоставление не проходит:
    class NotMatch(Exception):
        """Not match"""
    
    def not_match(x):
        raise NotMatch(x)
    

    И функцию, которая делает проверку и возвращает объект, либо бросает exception:
    match = lambda check, obj: obj if check(obj) else not_match(obj)
    match_curry = curry_n(match, 2)
    

    Теперь мы можем создать проверку типа:
    instance_of = lambda type_: match_curry(lambda obj: isinstance(obj, type_))
    

    Тогда для int:
    is_int = instance_of(int)
    print(is_int(2))  # 2
    try:
        is_int('str')
    except NotMatch:
        print('not int')  # not int
    

    Создадим проверку типа для списка, проверяя его каждый элемент:
    is_array_of = lambda matcher: match_curry(lambda obj: all(map(matcher, obj)))
    

    И тогда для int:
    is_array_of_int = is_array_of(is_int)
    print(is_array_of_int([1, 2, 3]))  # 1 2 3
    try:
        is_array_of_int('str')
    except NotMatch:
        print('not int')  # not int
    

    И теперь аналогично для str:
    is_str = instance_of(str)
    is_array_of_str = is_array_of(is_str)
    

    Также добавим функцию, возвращающую свой аргумент, идемпотентную =)
    identity = lambda x: x
    print(identity(10))  # 10
    print(identity(20))  # 20
    

    И проверку на пустой список:
    is_blank = match_curry(lambda xs: len(xs) == 0)
    print(is_blank([]))  # []
    try:
        is_blank([1, 2, 3])
    except NotMatch:
        print('not blank')  # not blank
    

    Теперь создадим функцию для разделения списка на первый элемент и остаток с применением к ним «проверок»:
    def hd_tl(match_x, match_xs, arr):
        x, *xs = arr
        return match_x(x), match_xs(xs)
    
    hd_tl_partial = lambda match_x, match_xs: partial(hd_tl, match_x, match_xs)
    

    И рассмотрим самый простой пример с identity:
    hd_tl_identity = hd_tl_partial(identity, identity)
    print(hd_tl_identity(range(5)))  # 0 [1, 2, 3, 4]
    

    А теперь с числами:
    hd_tl_ints = hd_tl_partial(is_int, is_array_of_int)
    print(hd_tl_ints(range(2, 6)))  # 2 [3, 4, 5]
    try:
        hd_tl_ints(['str', 1, 2])
    except NotMatch:
        print('not ints')  # not ints
    


    А теперь саму функцию, которая будет перебирать все проверки. Она очень простая:
    def pattern_match(patterns, args):
        for pattern, fnc in patterns:
            try:
                return fnc(pattern(args))
            except NotMatch:
                continue
        raise NotMatch(args)
    
    pattern_match_curry = curry_n(pattern_match, 2)
    



    Но зато она неудобна в использовании и требует целый мир скобок, например, нужная нам функция будет выглядеть так:
    sum_or_multiply = pattern_match_curry((
        (hd_tl_partial(identity, is_blank), lambda arr: arr[0]),  # x::[] -> x
        (hd_tl_ints, lambda arr: arr[0] * sum_or_multiply(arr[1])),  # x::xs -> x * sum_or_multiply (xs) где type(x) == int
        (hd_tl_partial(is_str, is_array_of_str), lambda arr: arr[0] + sum_or_multiply(arr[1])),  # x::xs -> x + sum_or_multiply (xs) где type(x) == str
    ))
    

    Теперь проверим ее в действии:
    print(sum_or_multiply(range(1, 10)))  # 362880
    print(sum_or_multiply(['a', 'b', 'c']))  # abc
    

    Ура! Оно работает =)

    Рекурсия


    Во всех классных языках программирования крутые ребята реализуют map через рекурсию, чем мы хуже? Тем более мы уже умеем pattern matching:
    r_map = lambda fn, arg: pattern_match((
        (hd_tl_partial(identity, is_blank), lambda arr: [fn(arr[0])]),  # x::[] -> fn(x)
        (
            hd_tl_partial(identity, identity),
            lambda arr: [fn(arr[0])] + r_map(fn, arr[1])  # x::xs -> fn(x)::r_map(fn, xs)
        ),
    ), arg)
    
    print(r_map(lambda x: x**2, range(10)))  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    

    Теперь каррируем:
    r_map_curry = curry_n(r_map, 2)
    twice = r_map_curry(lambda x: x * 2)
    print(twice(range(10)))  # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
    try:
        print(twice(range(1000)))
    except RuntimeError as e:
        print(e)  # maximum recursion depth exceeded in comparison
    

    Что-то пошло не так, попробуем хвостовую рекурсию.
    Для этого создадим «проверку» на None:
    is_none = match_curry(lambda obj: obj is None)
    

    И проверку пары:
    pair = lambda match_x, match_y: lambda arr: (match_x(arr[0]), match_y(arr[1]))
    

    А теперь и сам map:
    def r_map_tail(fn, arg):
        aux = lambda arg: pattern_match((
            (pair(identity, is_none), lambda arr: aux([arr[0], []])),  # если аккумулятор None, делаем его []
            (
                pair(hd_tl_partial(identity, is_blank), identity),
                lambda arr: arr[1] + [fn(arr[0][0])]  # если (x::[], acc), то прибавляем к аккумулятору fn(x) и возвращаем его
            ),
            (
                pair(hd_tl_partial(identity, identity), identity),
                lambda arr: aux([arr[0][1], arr[1] + [fn(arr[0][0])]])  # если (x::xs, acc), то делаем рекурсивный вызов с xs и аккумулятором + fn(x)
            ),
        ), arg)
        return aux([arg, None])
    

    Теперь опробуем наше чудо:
    r_map_tail_curry = curry_n(r_map_tail, 2)
    twice_tail = r_map_tail_curry(lambda x: x * 2)
    print(twice_tail(range(10)))  # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
    try:
        print(twice_tail(range(10000)))
    except RuntimeError as e:
        print(e)  # maximum recursion depth exceeded
    

    Вот ведь незадача — python не оптимизирует хвостовую рекурсию. Но теперь на помощь нам придут костыли:
    def tail_fnc(fn):
        called = False
        calls = []
    
        def run():
            while len(calls):  # вызываем функцию с аргументами из списка
                res = fn(*calls.pop())
            return res
    
        def call(*args):
            nonlocal called
            calls.append(args)  # добавляем аргументы в список
            if not called:  # проверяем вызвалась ли функция, если нет - запускаем цикл
                called = True
                return run()
        return call
    

    Теперь реализуем с этим map:
    def r_map_really_tail(fn, arg):
        aux = tail_fnc(lambda arg: pattern_match((  # декорируем вспомогательную функцию
            (pair(identity, is_none), lambda arr: aux([arr[0], []])),  # если аккумулятор None, делаем его []
            (
                pair(hd_tl_partial(identity, is_blank), identity),
                lambda arr: arr[1] + [fn(arr[0][0])]  # если (x::[], acc), то прибавляем к аккумулятору fn(x) и возвращаем его
            ),
            (
                pair(hd_tl_partial(identity, identity), identity),
                lambda arr: aux([arr[0][1], arr[1] + [fn(arr[0][0])]])  # если (x::xs, acc), то делаем рекурсивный вызов с xs и аккумулятором + fn(x)
            ),
        ), arg))
        return aux([arg, None])
    
    r_map_really_tail_curry = curry_n(r_map_really_tail, 2)
    twice_really_tail = r_map_really_tail_curry(lambda x: x * 2)
    print(twice_really_tail(range(1000)))  # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18...
    

    Теперь и это заработало =)

    Не все так страшно


    Если забыть про наш ужасный pattern matching, то рекурсивный map можно реализовать вполне аккуратно:
    def tail_r_map(fn, arr_):
        @tail_fnc
        def aux(arr, acc=None):
            x, *xs = arr
            if xs:
                return aux(xs, acc + [fn(x)])
            else:
                return acc + [fn(x)]
        return aux(arr_, [])
    
    curry_tail_r_map = curry_2(tail_r_map)
    


    И сделаем на нем умножение всех нечетных чисел в списке на 2:
    @curry_tail_r_map
    def twice_if_odd(x):
        if x % 2 == 0:
            return x * 2
        else:
            return x
    
    print(twice_if_odd(range(10000)))  # [0, 1, 4, 3, 8, 5, 12, 7, 16, 9, 20, 11, 24, 13, 28, 15, 32, 17, 36, 19...
    

    Получилось вполне аккуратно, хоть медленно и ненужно. Как минимум из-за скорости. Сравним производительность разных вариантов map:
    from time import time
    
    checker = lambda x: x ** 2 + x
    limit = 10000
    start = time()
    xs = [checker(x) for x in range(limit)][::-1]
    print('inline for:', time() - start)
    
    start = time()
    xs = list(map(checker, range(limit)))[::-1]
    print('map:', time() - start)
    
    calculate = curry_tail_r_map(checker)
    start = time()
    xs = calculate(range(limit))[::-1]
    print('r_map without pattern matching:', time() - start)
    
    calculate = r_map_really_tail_curry(checker)
    start = time()
    xs = calculate(range(limit))[::-1]
    print('r_map with pattern matching:', time() - start)
    

    После чего получим:
    inline for: 0.011110067367553711
    map: 0.011012554168701172
    r_map without pattern matching: 3.7527310848236084
    r_map with pattern matching: 5.926968812942505
    Вариант с pattern matching'ом оказался самым медленным, а встроенные map и for оказались самыми быстрыми.

    Заключение


    Из этой статьи в реальных приложениях можно использовать, пожалуй, только каррирование. Остальное либо нечитаемый, либо тормозной велосипед =)
    Все примеры доступны на github.
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 18
    • +7
      Заключение прекрасно!
      • 0
        Тоже проходил этот курс и люблю питон. С выводами абсолютно согласен.
        • +1
          а я просто оставлю это здесь:

          if (lambda sort = lambda v: (
          lambda vec = list(v):
          lambda len = len(vec):
          lambda for_ = lambda r, f: map(f, r),
          swap = lambda i, j: map(vec.__setitem__, (i, j), (vec[j], vec[i])):
          lambda _ =
          for_(range(len), lambda i:
          for_(range(i, len), lambda j:
          vec[i] < vec[j] or swap(i, j))):
          list(vec))()()()():
          __import__(«sys»).stdout.write("%s\n" % sort([3,5,2,6,8]))
          )() or True:
          print «You can do more in one expression»
          • +1
            Это оно так плохо отображается из-за того, что я отхабреный? (Тег source присутствует)
            • 0
              Ага, теги только у захабренных.
            • +1
              Как бы сказал один мой старый друг проффесор: за усердие — зачет, за понимание — неуд.
              Кроме того, что сравниваем огурцы с бананами, еще и грубейшие ошибки при замерах:
              1) Во первых, вы хотя бы сравнивайте на одинаковых входных данных, а то у вас xs всегда разный, и int вырастает до wide-wide-int пока последний тест отработает. Т.е. нужно брать из одного xs = range(limit) и чистить результат xs перед каждым start = time(). Потому как тогда одинаковые входные данные, да и всегда отрабатывем garbage переменной xs снаружи замера.
              Правильный код для проверки под спойлером
              xsin = range(limit)
              xs = None
              
              start = time()
              xs = [checker(x) for x in xsin][::-1]
              print('inline for:', time() - start, [xs[0], xs[-1], len(xs)])
              
              xs = None
              
              start = time()
              xs = list(map(checker, xsin))[::-1]
              print('map:', time() - start, [xs[0], xs[-1], len(xs)])
              
              xs = None
              
              calculate = curry_tail_r_map(checker)
              start = time()
              xs = calculate(xsin)[::-1]
              print('r_map without pattern matching:', time() - start, [xs[0], xs[-1], len(xs)])
              ...
              
              Для сравнения — ваше (с переопределением xs):
              inline for: [9999900000, 0, 100000]
              map: [0, 99998000019999900000, 100000]
              r_map without pattern matching: [9999600007999900000899994000029999900000, 0, 100000]
              
              А правильно:
              inline for: [9999900000, 0, 100000]
              map: [9999900000, 0, 100000]
              r_map without pattern matching: [9999900000, 0, 100000]
              

              Кстати в этом случае «map» будет всегда несколько быстрее «inline for».

              2) Вы компилировали это, перед исполнением? Потому что у меня несколько другие результаты (для 10 тысяч):
              inline for: 0.005000114440917969 [99990000, 0, 10000]
              map: 0.004999876022338867 [99990000, 0, 10000]
              r_map without pattern matching: 0.5000200271606445 [99990000, 0, 10000]
              r_map with pattern matching: 0.9875400066375732 [99990000, 0, 10000]

              А теперь внимание, с лимитом в 100 тысяч:
              inline for: 0.0650029182434082 [9999900000, 0, 100000]
              map: 0.05250191688537598 [9999900000, 0, 100000]
              r_map without pattern matching: 50.784531116485596 [9999900000, 0, 100000]
              r_map with pattern matching: 85.34341406822205 [9999900000, 0, 100000]

              3) Про вашу реализацию на хвостовой рекурсии я умолчу лучше. Кроме того, в следующий раз вы возьмите пример, который нельзя сделать с «inline for», без накладных на вызововы функций, стек и т.д.
              Т.е. реальный рекурсивный такой (пусть даже хвостовой) пример. Имхо, сравнение будет хотя бы чуть-чуть адекватнее.

              Выводы улыбнули…
              • 0
                1 — поправил, когда писал статью, не заметил. Но порядки результатов особо не поменялись.
                2 — запускал, машины же у всех разные и скорость выполнения разная.
                3:
                Про вашу реализацию на хвостовой рекурсии я умолчу лучше

                Предложите свою =)
                Т.е. реальный рекурсивный такой (пусть даже хвостовой) пример. Имхо, сравнение будет хотя бы чуть-чуть адекватнее.

                Не хвостовую, да и не всякую хвостовую в for(да и map, да и в reduce) можно переписать. Предложите свой пример.
                • 0
                  По 2 туплю, в статье 0 лишний был.
                  • +1
                    Да, нет, вы взгляните на мой пример еще раз: при увеличении количества итераций в 10 раз, время исполнения на «for» и «map» растет пропорционально (в 10 раз), два же последних теста замедлились в СТО раз — они не линейны, а значит что-то заведомо-изначально не верно!
                    • 0
                      Добавил подсчёт количества вызовов checker'а в github.com/nvbn/pyfunc/blob/master/pyfunc/timing.py, получилось:

                      inline for: 0.015415668487548828  called: 20000
                      map: 0.015140533447265625  called: 20000
                      r_map without pattern matching: 3.7559814453125  called: 20000
                      r_map with pattern matching: 5.883073806762695  called: 20000
                      

                      Всё вызывается одинаковое количество раз, теперь добавляю:

                      Результат:
                      inline for: 0.007587909698486328  called: 10000
                      map: 0.007678031921386719  called: 10000
                      r_map without pattern matching: 0.7014162540435791  called: 10000
                      r_map with pattern matching: 1.3289029598236084  called: 10000
                      straightforward map: 0.6855792999267578  called: 10000
                      straightforward map 2: 0.27460575103759766  called: 10000
                      straightforward map 3: 0.02640247344970703  called: 10000
                      

                      Так что не моя реализация кривая, а tuple unpacking и создание нового списка — медленные операции, и как раз создание нового списка походу n^2 =)
                      • 0
                        Вы серьезно? Никто и не сомневался что checker вызывается одинаковое количество раз. Я как-раз про накладные в вашей реализации обертки. И про адекватность сравнения этого с примитивами, на не совсем удачном примере. Но даже не беря это во внимание, стоимость алгоритма для вашего примера должна рости линейно.

                        Теперь насчет рекурсии.
                        Не хвостовую, да и не всякую хвостовую в for(да и map, да и в reduce) можно переписать.
                        Любую рекурсию можно развернуть в цикл, уменьшая тем самым накладные на вызов, передачу параметров, обертки scope и т.д. Пример ниже показывает рекурсию на цикле, где параметер «a» постоянный (например лямбда сравнения), «b» и «c» меняются (например бегут по хиерархии сравнивая два дерева). Стоимость «обертки» в таком случае зависит не от количества всех ветвей или того хуже листьев дерева, а от средней глубины его.
                        def recursive_proc (a, b, c):
                            stack = []
                            while 1:
                                ## делаем работу с текущими a, b,c :
                                ...
                                if next_level:
                                    ## push - след. уровень, текущие аргументы в стек :
                                    stack += [[b, c]]
                                    ## новые b и c
                                    b, c = ...
                                    continue;
                                else:
                                    ## pop - возврат в пред. уровень :
                                    b, c = stack.pop()
                                    ## окончание рекурсии :
                                    if not len(stack):
                                        break
                        

                        Создавать же рекурсию на сериях, безконечно создавая и копируя списки — это вообще что-то. При этом заявляя, что дескать, не моя реализация хромает, а питон медлено списки создает — это извините вообще по детски.
                        • 0
                          Я как-раз про накладные в вашей реализации обертки

                          Тормозная не обёртка, а сама оборачиваемая функция.

                          Любую рекурсию можно развернуть в цикл

                          Ну да, но не в inline for, map или reduce.

                          Создавать же рекурсию на сериях, безконечно создавая и копируя списки — это вообще что-то.

                          Статья разве называется «самая оптимальная реализация»?) Ну и если отказаться от копирования списка, то aux становится грязной.
                          • 0
                            Не поймите привратно — вы на самом деле большой молодец (многие из известных мне питон-программистов не поймут и половину того, что вы написали) и понятно, что делали вы это все, чтобы «повторить все эти крутые штуки в python». Вам бы еще чуть глубже в него закопатся, да некоторое представление — как все это в байт-коде работает — цены бы вам не было.
                            Книгу «Functional JavaScript» к сожалению не читал, но теперь (спасибо вам) представляю о чем речь. Другое дело, что у JS и python абсолютно разная концептуальная база (не знаю как правильно выразиться) и иногда так «напрямую» переделывать, имхо, как минимум не целесообразно. Например, у питона модель итераторов развита на порядок выше чем у JS. Может быть, используя их (или что-то другое, в чем питон «сильнее» JS), вы пришли бы к другим результатам и соответственно другим выводам.
                            • 0
                              Скажите, а насколько развита модель итераторов в Haskell, Erlang, OCaml или другом функциональном языке? Даже иначе поставлю вопрос — в этих языках они (итераторы) вообще есть?

                              Это, если что, я намекаю на то, что итераторы не совсем ФП.
                              • 0
                                Это не ко мне — не один из приведенных не знаю к сожалению настолько глубоко, чтобы коментировать уровень «развития» итераторов. Но судя по тому, что в Haskell'е например возможны следующие конструкции "iterate f x == [x, f x, f (f x), ...]" — бесконечный список повторяющихся приложений f от x — думается, что он (уровень) очень и очень на высоте. Хотя это тоже как посмотреть, конструкция ниже например для подсчета суммы через итератор далеко не идеальна (в исполняемом виде):
                                sumList :: [Integer] -> Integer
                                sumList (x:xs) = x + (sumList xs)
                                sumList [] = 0
                                
                                Проблема здесь в том, что ей требуется O(n) от размера передаваемого списка, выделения которой в этом случае можно было бы избежать, например как здесь (как раз вида хвостовой рекурсии):
                                sumList :: [Integer] -> Integer
                                sumList lst = sumLoop lst 0 where
                                    sumLoop (x:xs) i = sumLoop xs (i+x)
                                    sumLoop [] i = i
                                
                                Здесь sumLoop в отличии от первого примера использует тот же scope (stack frame), поэтому память «расходуется» sumList гораздо экономней.
                                Не зная такие тонкости можно огрести очень много проблем и со скоростью и с ресурсоемкостью. Это наверное одна из причин почему у меня душа не лежит ко всем вами перечисленным языкам. А может я просто не умею их готовить и потому больше «специализируюсь» по питону, тиклю и ко. Хотя тот же тикль например стандартно не имеет конструкций итератора (правда они неплохо строятся на лямбдах и синглтонах). Зато у него много других достоинств.
                                А вообще, имхо, лучше досконально знать один язык, чем поверхностно много.
                                • 0
                                  Ок. Если вы не поняли — в Haskell нету итераторов. Есть ленивые списки. А в Python нету истинно ленивых списков — есть только бесконечно-итерируемые коллекции. Так что в этом плане Python не намного лучше JS.
                                  • 0
                                    в Haskell нету итераторов
                                    Да ну? А с этим вот как?
                                    А в Python нету истинно ленивых списков
                                    В питоне я вам любой ленивый список на генераторах или их помеси с итераторами построю. В JS например этого в чистом виде (без выкрутасов) нет вовсе.
                                    • 0
                                      Итераторы в Python вещь замечательная. Только вот итераторы одноразовые в том смысле, что обладают побочным эффектом. А генераторы в этом плане еще хуже. Простите, о каком ФП может в таком случае идти речь.

                                      Сравните это, скажем, с приведенным выше классом Traversable. Что же нам теперь, все языки где есть абстракция для обхода коллекции называть функциональными?

                                      То, что в питоне есть итераторы, куча хороших функий для работы с ними — это не делает питон более/менее пригодным для ФП. Это элементы ФП, ни больше, ни меньше.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.