Пустая истина (3)
После второго раунда обсуждений стало ясно, почему every от пустого множества дает истину. Читатель Миша Левченко внятное объяснение, которое понятно мне как программисту. Оно не опирается на кванторы и логику. И хотя вывод все равно не нравится, приведу объяснение здесь.
Дело в том, что операции над списками нужно рассматривать как свертку. Есть
такая функция reduce
(она же fold
), которая принимает функцию двух
аргументов и коллекцию. Результат функции такой:
fn(fn(fn(fn(item0, item1), item2), item3), item4)...
Например, для сложения чисел 1, 2, 3, 4 получим форму:
(((1 + 2) + 3) + 4)
Reduce
может накапливать в том числе другую коллекцию: словарь или список. Это
вообще очень мощная функция. Про себя я называю ее царицей функций, потому что
через reduce можно выразить что угодно.
Reduce выше прекрасно работает, если элементов два и более. Когда их один или
ноль, начинаются граничные случаи. Одно из решений в том, что reduce
может
принимать т.к. init
— первичный элемент, который подставляется в начало
цепочки. Чаще всего он выступает коллекцией-аккумулятором, но может быть и
простым скаляром.
Если передать init
, форма будет такой:
fn(fn(fn(init, item0), item1), item2)...
Другими словами, он гарантирует, что элементов больше нуля. Если основной список пустой, просто вернем init.
Так вот, в терминах свертки функция ALL
(которую я раньше называл every?
)
выглядит так:
(func ALL [fn-pred items]
(reduce (fn [x y]
(and x (fn-pred y)))
true
items))
Демо:
(ALL int? [1 2 3])
true
(ALL int? [1 nil 3])
false
(ALL int? [])
true
А вот функция ANY
(что хотя бы один элемент вернул истину для предиката):
(func ANY [fn-pred items]
(reduce (fn [x y]
(or x (fn-pred y)))
false
items))
Демо:
(ANY int? [1 nil 3])
true
(ANY int? [nil nil nil])
false
(ANY int? [])
false
Аналогично работают функции суммирования: это reduce
, где начальные элементы
равны 0 и 1. Поэтому (+)
дает 0, а (*)
— 1.
Как видим, все это можно объяснить без греческих букв и терминов. А пустая истина, о которой я писал ранее, считается истиной только потому, что таков начальный элемент свертки.
Другое дело, что такой подход все равно мне не нравится. В каждом их них скрыт
начальный элемент: для ALL
— true
, для ANY
— false
, единица для
умножения и так далее. Считается очевидным, что он должен быть именно таким. А
мне это не очевидно. Я спотыкаюсь, когда вижу, что произведение элементов
пустого списка равно единице. Я бы предпочел неопределённость — то есть null.
Я в курсе про нейтральный элемент: ноль для сложения, единица для умножения. Но на пустых списках это как-то не очень. Душа не принимает, если совсем честно.
В самом деле, в математике оператор умножения — бинарный, ему нужно два
операнда. Нельзя записать что-то вроде 5 * = 5
— тут не хватает операнда
справа. С какой стати мы обходим математические правила — не ясно.
Я часто использую reduce
и вывел правило: всегда указываю начальный
элемент. Например, чтобы сложить список чисел, я пишу так:
(reduce + 0 numbers)
вместо
(apply + numbers)
Потому что во втором случае не очевидно, во что накапливается результат.
Словом, пока что меня отпустило на тему пустой истины. Все оказалось просто: это
свертка, где начальный элемент — истина. Крайне неочевидно, на мой взгляд. Чтобы
не отстрелить ногу, либо проверяйте коллекцию на пустоту, либо пишите явный
reduce
, где начальный элемент — ложь, если того требует контекст.
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter