Могу предложить вам посмотреть на интерфейс командной строки утилиты zenity — для простых случаев меню/диалогов в 1-2-3 уровня вложенности — самое оно! И можно прикрутить в текущую реализацию, например в качестве CLI при запуске библиотеки в роли скрипта.
В себе пока не чувствую сил теоркат объяснять, увы…
А for/yield это не просто map, это скорее filter/map/concat в одном флаконе:
class Book(title: String, authors: List[String])
// наименования книг, написанных Кнутом
for (b <- books; a <- b.authors if a startsWith "Knuth,") yield b.title
// авторы, написавшие более чем одну книгу
{ for {
b1 <- books
b2 <- books
if b1 != b2
a1 <- b1.authors
a2 <- b2.authors
if a1 == a2
} yield a1
}.distinct
В замечательной книге «Learn You a Haskell for Great Good!» отлично всё написано про монады, моноиды, функторы. Причем описание идёт на практических примерах — без непосредственного введения термина, скажем «монада», но с постепенным «выкристализовыванием» чего-то простого, понятного, а главное — полезного. А потом это «что-то» и оказывается монадой ))
Я думаю, не стоит цитировать книгу на Хабре — она есть в свободном доступе (по крайней мере английская версия). Своими словами можно объяснить, но лучше чем в книге у меня лично вряд ли получится…
P.S. Option в Scala, это, всё таки, просто АТД, в который удобно завернуть результат вычисления, способного неудасться. И всё. Синтаксической поддержки нет (удобной, хотя бы как в Haskell), вычисления в контексте монадном почти никто не делает…
Если уж монады в Scala искать, так лучше обратить внимание на for/yield — вполне себе, монада списка сдобренная нужным кол-вом синтаксического сахара.
P.P.S. Объяснение «на пальцах» выше указанным терминам можно дать в практическом ключе, а фундамент находится в теории категорий, которую сходу на пальцах не объяснишь…
len/min/max — это не функции даже, это обёртки над вызовом __len__/__lt__/__gt__ (ну может min/max посложнее чуток — там на входе итерируемый источник)
Т.е. len() возвращает длину объекта, который может её предоставить, min()/max() сравнивают сравнимые объекты, а их sort() сортирует, int()/str() приводит к целому/строке те объекты, которые сами могут представлять себя целым/строкой. Всё логично.
А range() именно такой, потому что имеет [start] и [step] — необязательные параметры, и такая сигнатура в общем случае не позволяет определить начальный элемент, и уж тем более шаг! Поэтому поддерживаемый типы аргументов ограничены числами, и начало/шаг имеют значения по-умолчанию. Ну и range() чаще всего используется в качестве источника индексов для прохода по индексируемому контейнеру, а тут как раз целые числа используются в качестве индекса, и начальный элемент контейнера обычно имеет индекс «0».
Если бы можно было ввести такие маг.методы:
__min__ — минимальный элемент данного типа
__max__ — максимальный элемент данного типа
__succ__(__pred__) — следующий(предыдущий) за текущим объектом элемент данного типа
можно было-бы реализовать range более всеядным — по-умолчанию отсчет шел бы от __min__, а инкремент происходил бы через __succ__. Всё это реализуемо, но встроенные типы данных — нерасширяемы, поэтому смысла особого нет. Проще под ситуацию написать свой my_range(), c датой и дельтами :)
P.S. Ну и в конце концов, в документации к range() указано, что он работает с числами — всё прозрачно.
В целом, «магические методы» — это в первую очередь Методы, и уж во вторую — «магические». И как любые методы — должны быть определены для объекта где-то в его иерархии наследования: «a + b» это, всего навсего, «a.__add__(b)», только с некоторой долей синтаксического сахара, отсюда и вся «волшебность». И для datetime определен метод __add__, который может принимать объект timedelta в качестве аргумента.
А range() — обычная функция, реализованная с поддержкой чисел и ничего кроме. При этом ф-ция имеет проверку на вызов для других типов данных — и возбуждает исключение, причем вполне конкретное, с объяснением причины!
Касательно магии сложения/вычитания: обычно операции сложения/вычитания работают над объектами одного типа (если для их класса реализовано соответствующее поведение), а datetime/timedelta, это скорее исключение: нельзя сложить две даты — это лишено смысла, но можно изменить дату на некоторое кол-во фиксированных единиц (дней/часов/минут...) и нельзя увеличить дату на n месяцев/лет — кол-во дней плавает.
range() работает только с числами и «магию» не использует (за пределами операций над числами).
При этом её аргумент «шаг» — необязателен. А как тогда как ф-ция должна определять, чем инкрементить дату?
Можно написать всеядную «магическую» версию range() (я напишу «ленивый» вариант):
Этот вариант работать будет для дат/времени (да и для списков, скажем), но потребует обязательного указания объекта-инкремента. Такое поведение неявно, ИМХО.
reversed и срезы не то чтобы пересекаются — они работают с разными «интерфейсами» объекта.
Срез работает через метод __getitem__() объекта, причем этот метод должен уметь принимать экземпляр slice в качестве параметра. reversed() работает только с объектами, которые имеют и __getitem__() и __len__(), но зато reversed() работает поэлементно и делает это лениво (итератор). При этом reversed() вызывает __getitem__() с целочисленным аргументом.
Таким образом, reversed(), в общем случае, предполагает, что источником данных для него будет объект-контейнер с произвольным доступом к содержимому.
А срез можно реализовать и для объектов, представляющих потоковые данные — через пропуски ненужных элементов. Правда без буферизации не будут работать отрицательные шаги и срез от конца. Так работает itertools.islice — итератор-срез от итерируемого источника
Ну и reversed()/срезы, это, так сказать, функциональный подход — результат отвязан от источника.
А reverse(), это ООП подход — метод объекта, изменяющий его состояние.
Y-комбинатор позволяет в анонимной функции использовать рекурсию с использованием самой себя, без необходимости назначения ей имени — функция остаётся анонимной.
Скажем, у нас есть неанонимная рекурсивная функция вычисления факториала:
def fact(n):
return n * fact(n-1) if n > 0 else 1
print fact(10)
Это работает, т.к. у нашей функции есть имя fact и мы можем по нему функцию вызывать внутри неё самой.
Но если мы захотим написать анонимную функцию вычисления факториала, мы столкнемся с проблемой — как нам внутри функции вызвать её саму, не имея имени.
Можно попробовать сделать так:
fact = lambda self, n: n * self(self, n) if n > 0 else 1
print fact(fact, 10)
В данном случае имя функции не встречается внутри неё. Но при внешнем вызове функции приходится передавать в качестве одного из аргументов ссылку на неё саму. Это можно обернуть так:
wrap = lambda fn, *args: fn(fn, *args)
fact = lambda self, n: n * self(self, n) if n > 0 else 1
print wrap(fact, 10)
Теперь мы её ещё немного дообернем:
y = lambda fn: lambda *args: fn(fn, *args)
fact = y(lambda self, n: n * self(self, n) if n > 0 else 1)
Вот y и есть Y-комбинатор, к тому же анонимный. Пользуем inline как то так:
# применяем анонимную рекурсивную функцию
map(
(lambda fn: lambda *args: fn(fn, *args))(
lambda self, n: n * self(self, n-1) if n > 0 else 1),
[1,2,3]
)
Ну правильнее лучше чем короче )) Да и метакласс не сильно длиннее вашего класса будет. Зато метакласс можно подмешать (например через декоратор) к любому (ну почти) существующему классу, не навязывая наследование. Это несомненно плюс!
Тем не менее аргументы могут понадобиться, как параметры создания первого экземпляра, поэтому всё же лучше предоставить возможность, а не наложить ограничение (ИМХО).
А повторный вызов с другими аргументами можно перенаправить в явный метод типа _update(...). Скажем у нас есть сквозной singleton-журнал, и повторное «инстанцирование» со строкой-параметром дополняет уже существующий журнал.
Опять же через параметры конструктора можно реализовать не singleton, но cache, ключами которого будут хэши параметров
Между прочим, предыдущий вариант на метаклассе был лучше: в данной реализации __init__ будет вызываться при каждом инстанцировании класса, вне зависимости от того новый экземпляр возвращается или ранее созданный! Т.о. придётся инициализацию переносить в метод типа __init2__ и вызывать вручную в __new__ при создании нового объекта, а __init__ оставлять пустым. Ну и потомки этого класса не будут иметь возможности иметь аргументы инициализации.
Тоже думал над вертикальном расположении «исходников», но охрана может не только X охранять, но и Y вместе с ним. Правда, такую комплексную охрану можно уже после объединяющей скобки поставить.
— Clojure (изучаю, активно интересуюсь)
— Haskell (изучаю, активно интересуюсь)
— Scala (изучаю, активно интересуюсь)
— Erlang (небольшой опыт разработки)
— JS (начальный уровень)
— AVR C/С++ (хобби-проекты + опыт коммерческой разработки)
habrahabr.ru/users/astynax/
ru.linkedin.com/pub/alexey-pirogov/61/272/810/
А for/yield это не просто map, это скорее filter/map/concat в одном флаконе:
Я думаю, не стоит цитировать книгу на Хабре — она есть в свободном доступе (по крайней мере английская версия). Своими словами можно объяснить, но лучше чем в книге у меня лично вряд ли получится…
P.S. Option в Scala, это, всё таки, просто АТД, в который удобно завернуть результат вычисления, способного неудасться. И всё. Синтаксической поддержки нет (удобной, хотя бы как в Haskell), вычисления в контексте монадном почти никто не делает…
Если уж монады в Scala искать, так лучше обратить внимание на for/yield — вполне себе, монада списка сдобренная нужным кол-вом синтаксического сахара.
P.P.S. Объяснение «на пальцах» выше указанным терминам можно дать в практическом ключе, а фундамент находится в теории категорий, которую сходу на пальцах не объяснишь…
Т.е. len() возвращает длину объекта, который может её предоставить, min()/max() сравнивают сравнимые объекты, а их sort() сортирует, int()/str() приводит к целому/строке те объекты, которые сами могут представлять себя целым/строкой. Всё логично.
А range() именно такой, потому что имеет [start] и [step] — необязательные параметры, и такая сигнатура в общем случае не позволяет определить начальный элемент, и уж тем более шаг! Поэтому поддерживаемый типы аргументов ограничены числами, и начало/шаг имеют значения по-умолчанию. Ну и range() чаще всего используется в качестве источника индексов для прохода по индексируемому контейнеру, а тут как раз целые числа используются в качестве индекса, и начальный элемент контейнера обычно имеет индекс «0».
Если бы можно было ввести такие маг.методы:
__min__ — минимальный элемент данного типа
__max__ — максимальный элемент данного типа
__succ__(__pred__) — следующий(предыдущий) за текущим объектом элемент данного типа
можно было-бы реализовать range более всеядным — по-умолчанию отсчет шел бы от __min__, а инкремент происходил бы через __succ__. Всё это реализуемо, но встроенные типы данных — нерасширяемы, поэтому смысла особого нет. Проще под ситуацию написать свой my_range(), c датой и дельтами :)
P.S. Ну и в конце концов, в документации к range() указано, что он работает с числами — всё прозрачно.
А range() — обычная функция, реализованная с поддержкой чисел и ничего кроме. При этом ф-ция имеет проверку на вызов для других типов данных — и возбуждает исключение, причем вполне конкретное, с объяснением причины!
Касательно магии сложения/вычитания: обычно операции сложения/вычитания работают над объектами одного типа (если для их класса реализовано соответствующее поведение), а datetime/timedelta, это скорее исключение: нельзя сложить две даты — это лишено смысла, но можно изменить дату на некоторое кол-во фиксированных единиц (дней/часов/минут...) и нельзя увеличить дату на n месяцев/лет — кол-во дней плавает.
range() работает только с числами и «магию» не использует (за пределами операций над числами).
При этом её аргумент «шаг» — необязателен. А как тогда как ф-ция должна определять, чем инкрементить дату?
Можно написать всеядную «магическую» версию range() (я напишу «ленивый» вариант):
Этот вариант работать будет для дат/времени (да и для списков, скажем), но потребует обязательного указания объекта-инкремента. Такое поведение неявно, ИМХО.
Срез работает через метод __getitem__() объекта, причем этот метод должен уметь принимать экземпляр slice в качестве параметра.
reversed() работает только с объектами, которые имеют и __getitem__() и __len__(), но зато reversed() работает поэлементно и делает это лениво (итератор). При этом reversed() вызывает __getitem__() с целочисленным аргументом.
Таким образом, reversed(), в общем случае, предполагает, что источником данных для него будет объект-контейнер с произвольным доступом к содержимому.
А срез можно реализовать и для объектов, представляющих потоковые данные — через пропуски ненужных элементов. Правда без буферизации не будут работать отрицательные шаги и срез от конца. Так работает itertools.islice — итератор-срез от итерируемого источника
Ну и reversed()/срезы, это, так сказать, функциональный подход — результат отвязан от источника.
А reverse(), это ООП подход — метод объекта, изменяющий его состояние.
Скажем, у нас есть неанонимная рекурсивная функция вычисления факториала:
Это работает, т.к. у нашей функции есть имя fact и мы можем по нему функцию вызывать внутри неё самой.
Но если мы захотим написать анонимную функцию вычисления факториала, мы столкнемся с проблемой — как нам внутри функции вызвать её саму, не имея имени.
Можно попробовать сделать так:
В данном случае имя функции не встречается внутри неё. Но при внешнем вызове функции приходится передавать в качестве одного из аргументов ссылку на неё саму. Это можно обернуть так:
Теперь мы её ещё немного дообернем:
Вот y и есть Y-комбинатор, к тому же анонимный. Пользуем inline как то так:
PROFIT! :)
А повторный вызов с другими аргументами можно перенаправить в явный метод типа _update(...). Скажем у нас есть сквозной singleton-журнал, и повторное «инстанцирование» со строкой-параметром дополняет уже существующий журнал.
Опять же через параметры конструктора можно реализовать не singleton, но cache, ключами которого будут хэши параметров
(Тут у меня что-то типа стрелок сверху вниз, надеюсь, понятно будет)