Мені давно цікаво, чому корисна лінива оцінка. Мені ще доводиться комусь пояснювати мені так, що має сенс; в основному це закінчується кипінням, щоб "довірити мені".
Примітка: я не маю на увазі запам'ятовування.
Мені давно цікаво, чому корисна лінива оцінка. Мені ще доводиться комусь пояснювати мені так, що має сенс; в основному це закінчується кипінням, щоб "довірити мені".
Примітка: я не маю на увазі запам'ятовування.
Відповіді:
Переважно тому, що це може бути більш ефективно - значення не потрібно обчислювати, якщо вони не збираються використовувати. Наприклад, я можу передавати три значення у функції, але залежно від послідовності умовних виразів насправді може використовуватися лише підмножина. У такій мові, як C, усі три значення були б обчислені в будь-якому випадку; але в Haskell обчислюються лише необхідні значення.
Це також дозволяє класні речі, як нескінченні списки. Я не можу мати нескінченний список такою мовою, як C, але в Haskell це не проблема. Нескінченні списки використовуються досить часто в певних областях математики, тому може бути корисним можливість маніпулювати ними.
Корисним прикладом лінивої оцінки є використання quickSort
:
quickSort [] = []
quickSort (x:xs) = quickSort (filter (< x) xs) ++ [x] ++ quickSort (filter (>= x) xs)
Якщо ми хочемо знайти мінімум списку, можемо визначитись
minimum ls = head (quickSort ls)
Котрий спочатку сортує список, а потім бере перший елемент списку. Однак через ледачу оцінку обчислюється лише голова. Наприклад, якщо ми візьмемо мінімум списку [2, 1, 3,]
quickSort спочатку відфільтрує всі елементи, менші ніж два. Тоді це робить швидкоSort на цьому (повертаючи одиночний список [1]), що вже достатньо. Через ледачу оцінку, решта ніколи не сортується, економлячи багато обчислювального часу.
Це, звичайно, дуже простий приклад, але лінь працює так само і для дуже великих програм.
Однак, у всьому цьому є і мінус: важче передбачити швидкість виконання та використання пам'яті вашої програми. Це не означає, що ліниві програми повільніші або займають більше пам’яті, але це добре знати.
take k $ quicksort list
займає лише O (n + k log k) n = length list
. З не лінивим сортуванням порівняння це завжди потребуватиме часу O (n log n).
Я вважаю ледачу оцінку корисною для ряду речей.
По-перше, всі існуючі ледачі мови є чистими, тому що дуже важко міркувати про побічні ефекти в ледачій мові.
Чисті мови дозволяють міркувати про визначення функцій, використовуючи екваціональне міркування.
foo x = x + 3
На жаль, в умовах, що не лінуються, більше заяв не повертається, ніж у ледачих умовах, тому це менш корисно для мов, таких як ML. Але ледачою мовою можна сміливо міркувати про рівність.
По-друге, багато речей, таких як "обмеження значення" в ML, не потрібні в ледачих мовах, як Haskell. Це призводить до великого розшарування синтаксису. ML, як мови, повинні використовувати такі ключові слова, як var або fun. У Haskell ці речі руйнуються до одного поняття.
По-третє, лінь дозволяє писати дуже функціональний код, який можна зрозуміти по шматочках. У Haskell прийнято писати тіло функції, як:
foo x y = if condition1
then some (complicated set of combinators) (involving bigscaryexpression)
else if condition2
then bigscaryexpression
else Nothing
where some x y = ...
bigscaryexpression = ...
condition1 = ...
condition2 = ...
Це дозволяє вам працювати «зверху вниз», хоча розуміння суті функції. Мови, подібні до ML, змушують вас використовувати let
суто оцінене. Отже, ви не наважуєтесь "підняти" пункт дозволу до основного складу функції, оскільки якщо це дорого (або має побічні ефекти), ви не хочете, щоб його завжди оцінювали. Haskell може "відсунути" деталі до пункту "явно", оскільки знає, що вміст цього пункту буде оцінено лише за необхідності.
На практиці ми, як правило, використовуємо охорону та обвал, що далі:
foo x y
| condition1 = some (complicated set of combinators) (involving bigscaryexpression)
| condition2 = bigscaryexpression
| otherwise = Nothing
where some x y = ...
bigscaryexpression = ...
condition1 = ...
condition2 = ...
По-четверте, лінь іноді пропонує набагато більш елегантне вираження певних алгоритмів. Ледачий «швидкий сорт» в Haskell - це однолінійний перелік, і якщо ви подивитесь лише на перші кілька пунктів, ви сплачуєте лише витрати, пропорційні витратам на вибір саме цих предметів. Ніщо не заважає вам робити це суворо, але вам, ймовірно, доведеться перекодувати алгоритм кожен раз, щоб досягти однакових асимптотичних показників.
По-п'яте, лінь дозволяє визначати нові структури управління мовою. Ви не можете написати нове "якщо .. тоді .. інше ..", як конструювати суворою мовою. Якщо ви спробуєте визначити функцію, наприклад:
if' True x y = x
if' False x y = y
суворою мовою тоді обидві гілки оцінюватимуться незалежно від значення умови. Це стає гірше, якщо врахувати петлі. Усі суворі рішення вимагають від мови, щоб надати вам якусь цитату або явну лямбда-побудову.
Нарешті, в цьому ж ключі деякі найкращі механізми боротьби з побічними ефектами в системі типів, такі як монади, справді можуть бути ефективно виражені лише в ледачих умовах. Це можна засвідчити, порівнюючи складність робочих процесів F # з москалами Haskell. (Ви можете визначити монаду суворою мовою, але, на жаль, ви часто провадите закон про монаду або два через відсутність ліні та робочих процесів, порівнявши, вибираєте тонну суворого багажу.)
let
Суворою мовою рекурсивна небезпечна тварина, в схемі R6RS вона дозволяє випадково #f
з'являтися у вашому терміні, де б не зав'язування вузла суворо призвело до циклу! Ніяких каламбурів, але строго більш рекурсивні let
прив’язки чутні на ледачій мові. Суворість також посилює той факт, що where
взагалі немає способу впорядкувати відносні ефекти, за винятком SCC, це побудова рівня тверджень, його ефекти можуть статися в будь-якому порядку строго, і навіть якщо у вас є чиста мова, ви закінчите #f
проблема. Суворі where
загадки вашого коду з немісцевими проблемами.
ifFunc(True, x, y)
буде оцінювати і те, x
і y
замість просто x
.
Існує різниця між нормальною оцінкою порядку та ледачою оцінкою (як у Haskell).
square x = x * x
Оцінка наступного виразу ...
square (square (square 2))
... з нетерплячою оцінкою:
> square (square (2 * 2))
> square (square 4)
> square (4 * 4)
> square 16
> 16 * 16
> 256
... із звичайною оцінкою замовлення:
> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * (square (square 2))
> ((2 * 2) * (square 2)) * (square (square 2))
> (4 * (square 2)) * (square (square 2))
> (4 * (2 * 2)) * (square (square 2))
> (4 * 4) * (square (square 2))
> 16 * (square (square 2))
> ...
> 256
... з ледачою оцінкою:
> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * ((square 2) * (square 2))
> ((2 * 2) * (2 * 2)) * ((2 * 2) * (2 * 2))
> (4 * 4) * (4 * 4)
> 16 * 16
> 256
Це тому, що лінива оцінка дивиться на синтаксичне дерево і робить перетворення дерева ...
square (square (square 2))
||
\/
*
/ \
\ /
square (square 2)
||
\/
*
/ \
\ /
*
/ \
\ /
square 2
||
\/
*
/ \
\ /
*
/ \
\ /
*
/ \
\ /
2
... тоді як звичайна оцінка порядку виконує лише текстові розширення.
Ось чому ми, використовуючи ледачу оцінку, отримуємо більш потужний характер (оцінювання закінчується частіше, ніж інші стратегії), тоді як ефективність еквівалентна прагненню оцінювання (принаймні, в O-нотації).
Ледача оцінка стосувалася процесора так само, як і збирання сміття, пов'язаного з оперативною пам’яттю. GC дозволяє зробити вигляд, що у вас є необмежений об'єм пам'яті, і таким чином вимагати стільки об'єктів у пам'яті, скільки вам потрібно. Час виконання автоматично відшкодовує непридатні об'єкти. LE дозволяє робити вигляд, що у вас необмежені обчислювальні ресурси - ви можете робити стільки обчислень, скільки вам потрібно. Час виконання просто не виконуватиме непотрібні (для даного випадку) обчислення.
У чому полягає практична перевага цих «прикидних» моделей? Він звільняє розробника (певною мірою) від управління ресурсами та видаляє з коду код код. Але важливіше те, що ви можете ефективно використовувати своє рішення в більш широкому наборі контекстів.
Уявіть, що у вас є список чисел S і число N. Потрібно знайти найближче до числа N число M зі списку S. Ви можете мати два контексти: одинарний N і деякий список L з Ns (ei для кожного N у L ти шукаєш найближчий М у S). Якщо ви використовуєте ледачу оцінку, ви можете сортувати S та застосувати двійковий пошук, щоб знайти найближчі M до N. Для гарного ледачого сортування знадобиться крок O (size (S)) для одиночних N та O (ln (size (S)) * (розмір (S) + розмір (L))) кроки для однаково розподіленого L. Якщо у вас немає ледачої оцінки для досягнення оптимальної ефективності, вам доведеться реалізувати алгоритм для кожного контексту.
Якщо вірити Саймону Пейтону Джонсу, лінива оцінка не важлива сама по собі, а лише як "сорочка для волосся", яка змусила дизайнерів зберегти мову чистою. Я вважаю симпатичним цій точці зору.
Річард Берд, Джон Х'юз і в меншій мірі Ральф Гінзе здатні робити дивовижні речі з ледачою оцінкою. Читання їхньої роботи допоможе вам оцінити її. Добрими вихідними точками є чудовий вирішувач судоку Bird та праця Х'юза на тему « Чому функціональне програмування має значення» .
IO
монади) підпис main
був би, String -> String
і ви вже могли правильно писати інтерактивні програми.
IO
монади?
Розгляньте програму з тик-так-носком. Це чотири функції:
Це створює приємне чітке розділення проблем. Зокрема, функція генерації рухів та функції оцінювання на дошці - єдині, хто потребує розуміння правил гри: дерево переміщення та функції мінімакс повністю використовувати багаторазово.
Тепер давайте спробуємо реалізувати шахи замість тик-нога. У "нетерплячій" (тобто звичайній) мові це не працюватиме, оскільки дерево переміщення не вміститься в пам'яті. Тож тепер оцінку дошки та функції генерації переміщень потрібно змішувати з деревом переміщення та логікою minimax, оскільки логіка minimax повинна використовуватися для вирішення, які ходи генерувати. Наша приємна чиста модульна структура зникає.
Однак в ледачій мові елементи дерева переміщення створюються лише у відповідь на вимоги функції minimax: все дерево переміщення не потрібно генерувати, перш ніж ми пустимо minimax на верхній елемент. Тож наша чиста модульна структура все ще працює в реальній грі.
Ось ще два моменти, які, на мою думку, ще не підняті в дискусії.
Лінь - це механізм синхронізації в одночасному середовищі. Це легкий і простий спосіб створити посилання на деякі обчислення та поділитися його результатами між багатьма потоками. Якщо кілька потоків намагаються отримати доступ до неоціненого значення, лише один з них виконає його, а інші заблокують відповідно, отримавши значення, як тільки воно стане доступним.
Лінь є основою для амортизації структур даних у чистому середовищі. Це описує Окасакі в " Чисто функціональних структурах даних" докладно, але основна ідея полягає в тому, що ледача оцінка є контрольованою формою мутацій, критичною для того, щоб ми могли ефективно реалізувати певні типи структур даних. Хоча ми часто говоримо про лінь, що змушує нас носити зачіску чистоти, інший спосіб також застосовується: вони є синергетичними особливостями мови.
Коли ви вмикаєте комп'ютер, і Windows утримується від відкриття кожної окремої папки на вашому жорсткому диску в Провіднику Windows і утримується від запуску кожної окремо встановленої на вашому комп'ютері програми, поки ви не вкажете, що потрібен певний каталог або потрібна певна програма, це "ледача" оцінка.
Оцінка "ледачого" - це виконання операцій, коли і як вони потрібні. Це корисно, коли це особливість мови програмування чи бібліотеки, оскільки загалом важче самостійно реалізувати оцінку, ніж просто попередньо підрахувати все наперед.
Врахуйте це:
if (conditionOne && conditionTwo) {
doSomething();
}
Метод doSomething () буде виконуватися лише в тому випадку, якщо conditionOne є істинним, а conditionTwo - істинним. У випадку, коли умова onene помилкова, навіщо потрібно обчислити результат умови два? Оцінка стануДвох буде марна трата часу в цьому випадку, особливо якщо ваш стан є результатом якогось методу.
Це один із прикладів зацікавленості в ледачих оцінках ...
Це може підвищити ефективність. Це очевидний вигляд, але насправді це не найважливіше. (Зауважте також, що лінь може вбити і ефективність - цей факт не відразу очевидний. Однак, зберігаючи безліч тимчасових результатів, а не обчислюючи їх відразу, ви можете використовувати величезну кількість оперативної пам’яті.)
Це дозволяє визначати конструкції управління потоком у звичайному коді на рівні користувача, а не бути жорстко закодованим мовою. (Наприклад, у Java є for
петлі; у Haskell є for
функція. У Java є обробка виключень; у Haskell є різні типи монади виключень. C # has goto
; Haskell має монаду продовження ...)
Це дозволяє роз'єднати алгоритм для генерації даних з алгоритму для вирішення кількості даних для генерації. Ви можете написати одну функцію, яка генерує умовно-нескінченний список результатів, та іншу функцію, яка обробляє стільки цього списку, скільки вирішує, що потрібно. Більш суттєво, ви можете мати п'ять функцій генератора і п'ять функцій споживача, і ви можете ефективно створити будь-яку комбінацію - замість кодування вручну 5 х 5 = 25 функцій, які поєднують обидві дії одночасно. (!) Ми всі знаємо, що розв'язка - це гарна річ.
Це більш-менш змушує вас розробити чисто функціональну мову. Завжди спокушати робити скорочення, але ледачою мовою найменша домішка робить ваш код дико непередбачуваним, що сильно протидіє застосуванню ярликів.
Однією з величезних переваг лінь є можливість писати незмінні структури даних з розумними амортизованими межами. Простий приклад - незмінний стек (за допомогою F #):
type 'a stack =
| EmptyStack
| StackNode of 'a * 'a stack
let rec append x y =
match x with
| EmptyStack -> y
| StackNode(hd, tl) -> StackNode(hd, append tl y)
Код є розумним, але додавання двох стеків x і y займає час O (довжина x) у кращому, гіршому та середньому випадках. Додавання двох стеків - це монолітна операція, вона торкається всіх вузлів у стеку x.
Ми можемо переписати структуру даних як лінивий стек:
type 'a lazyStack =
| StackNode of Lazy<'a * 'a lazyStack>
| EmptyStack
let rec append x y =
match x with
| StackNode(item) -> Node(lazy(let hd, tl = item.Force(); hd, append tl y))
| Empty -> y
lazy
працює, призупинивши оцінку коду у своєму конструкторі. Оцінившись за допомогою .Force()
, повернене значення кешується та повторно використовується для кожного наступного .Force()
.
У лінивій версії додавання - це операція O (1): вона повертає 1 вузол і призупиняє фактичну перебудову списку. Коли ви отримаєте заголовок цього списку, він оцінить вміст вузла, змусивши його повернути головку і створить одну суспензію з рештою елементами, тому прийняття заголовка списку - це операція O (1).
Отже, наш лінивий список знаходиться в постійному стані перебудови, ви не платите витрати на перебудову цього списку, поки не пройдете всі його елементи. Використовуючи лінь, цей список підтримує надходження та додавання O (1). Цікаво, що оскільки ми не оцінюємо вузли до їх доступу, цілком можливо побудувати список з потенційно нескінченними елементами.
Структура даних, наведена вище, не вимагає перерахунку вузлів для кожного проходження, тому вони чітко відрізняються від ванільних IEnumerables у .NET.
Цей фрагмент показує різницю між лінивою та не лінивою оцінкою. Звичайно, ця функція напруженості може бути оптимізована і використовувати ледачу оцінку замість рекурсії, але це зіпсує приклад.
Припустимо, ми МОЖЕТЕ для чогось використовувати 20 перших чисел, при цьому не лінива оцінка всіх 20 чисел повинна бути сформована вперед, але, при ледачій оцінці, вони створюватимуться лише за потреби. Таким чином, ви заплатите лише розрахункову ціну при необхідності.
Вибірка зразка
Не ліниве покоління: 0,023373 Ледаче покоління: 0,000009 Не лінивий вихід: 0,000921 Ледачий вихід: 0,024205
import time
def now(): return time.time()
def fibonacci(n): #Recursion for fibonacci (not-lazy)
if n < 2:
return n
else:
return fibonacci(n-1)+fibonacci(n-2)
before1 = now()
notlazy = [fibonacci(x) for x in range(20)]
after1 = now()
before2 = now()
lazy = (fibonacci(x) for x in range(20))
after2 = now()
before3 = now()
for i in notlazy:
print i
after3 = now()
before4 = now()
for i in lazy:
print i
after4 = now()
print "Not lazy generation: %f" % (after1-before1)
print "Lazy generation: %f" % (after2-before2)
print "Not lazy output: %f" % (after3-before3)
print "Lazy output: %f" % (after4-before4)
Ледаче оцінювання найбільш корисне для структур даних. Ви можете визначити масив чи вектор індуктивно, вказуючи лише певні точки структури та виражаючи всі інші за допомогою всього масиву. Це дозволяє створювати структури даних дуже стисло та з високою продуктивністю.
Щоб побачити це в дії, ви можете подивитися на мою бібліотеку нейронних мереж під назвою інстинкт . Це дуже сильно використовує ліниві оцінки для елегантності та високої продуктивності. Наприклад, я повністю позбавляюся від традиційно необхідного розрахунку активації. Простий ледачий вираз робить для мене все.
Це використовується, наприклад, у функції активації, а також в алгоритмі навчання зворотного розповсюдження (я можу розміщувати лише два посилання, тому вам потрібно буде самостійно шукати learnPat
функцію в AI.Instinct.Train.Delta
модулі). Традиційно обидва вимагають значно складніших ітеративних алгоритмів.
Інші люди вже дали великі причини, але я вважаю корисною вправою допомогти зрозуміти, чому лінь має значення - спробувати записати функцію з фіксованою точкою суворою мовою.
У Haskell функція з фіксованою точкою дуже проста:
fix f = f (fix f)
це розширюється до
f (f (f ....
але оскільки Хаскелл лінивий, цей нескінченний ланцюжок обчислень не є проблемою; оцінка робиться "зовні" всередину ", і все працює чудово:
fact = fix $ \f n -> if n == 0 then 1 else n * f (n-1)
Важливо, що важливо не те, щоб fix
бути ледачим, а тим, що f
буде лінивим. Після того, як ви вже отримали строгий f
, ви можете кинути руки в повітря і здатися, або ета розгорнути його і захарастити речі. (Це дуже схоже на те, що Ной говорив про те, що це бібліотека, яка сувора / ледача, а не мова).
А тепер уявіть, як писати ту саму функцію у строгій Scala:
def fix[A](f: A => A): A = f(fix(f))
val fact = fix[Int=>Int] { f => n =>
if (n == 0) 1
else n*f(n-1)
}
Ви, звичайно, отримуєте переповнення стека. Якщо ви хочете, щоб він працював, вам потрібно зробити f
аргумент за потребою:
def fix[A](f: (=>A) => A): A = f(fix(f))
def fact1(f: =>Int=>Int) = (n: Int) =>
if (n == 0) 1
else n*f(n-1)
val fact = fix(fact1)
Я не знаю, як ви зараз думаєте про речі, але мені здається корисним вважати ледаче оцінювання як проблему бібліотеки, а не мовної функції.
Я маю на увазі, що в строгих мовах я можу реалізувати ледачу оцінку, побудувавши декілька структур даних, а в ледачих мовах (принаймні Haskell) я можу попросити суворості, коли я цього хочу. Тому вибір мови насправді не робить ваші програми ледачими чи не лінивими, а просто впливає на те, що ви отримуєте за замовчуванням.
Коли ви думаєте про це так, то подумайте про всі місця, де ви пишете структуру даних, яку згодом ви можете використовувати для генерування даних (не надто переглядаючи їх до цього часу), і ви побачите безліч застосувань для ледачих оцінка.
Найкориснішою експлуатацією лінивої оцінки, яку я використав, була функція, яка називала серію підфункцій у певному порядку. Якщо будь-яка з цих підфункцій не вдалася (повернулася помилково), функцію виклику потрібно було негайно повернути. Тож я міг би зробити це так:
bool Function(void) {
if (!SubFunction1())
return false;
if (!SubFunction2())
return false;
if (!SubFunction3())
return false;
(etc)
return true;
}
або, більш елегантне рішення:
bool Function(void) {
if (!SubFunction1() || !SubFunction2() || !SubFunction3() || (etc) )
return false;
return true;
}
Як тільки ви почнете його використовувати, ви побачите можливості використовувати його все частіше та частіше.
Без ледачих оцінок вам не дозволять написати щось подібне:
if( obj != null && obj.Value == correctValue )
{
// do smth
}
Крім усього іншого, ледачі мови дозволяють багатовимірні нескінченні структури даних.
Хоча схема, python тощо дозволяють одновимірних нескінченних структурах даних з потоками, ви можете пройти лише один вимір.
Лінь корисна для тієї самої проблеми з бахромою , але варто відзначити з'єднання між собою, згадане в цьому посиланні.
Ледача оцінка - це еквівалентне міркування бідної людини (яке, в ідеалі, можна було б вивести властивостями коду з властивостей типів та операцій, що займаються).
Приклад , де вона працює досить добре: sum . take 10 $ [1..10000000000]
. Що ми не проти зменшити до суми 10 чисел, а не лише одного прямого і простого числового обчислення. Без лінивої оцінки, звичайно, це створило б гігантський список в пам'яті лише для використання перших 10 елементів. Це, безумовно, буде дуже повільним і може призвести до помилки поза пам'яттю.
Приклад , де це не так велика , як хотілося б: sum . take 1000000 . drop 500 $ cycle [1..20]
. Що насправді підсумовує 1 000 000 чисел, навіть якщо в циклі замість списку; все-таки його слід звести лише до одного прямого числового обчислення, маючи декілька умовних умов і мало формул. Що було б набагато краще, ніж підсумовувати 1 000 000 чисел. Навіть якщо в циклі, а не в списку (тобто після оптимізації вирубки лісів).
Інша справа, що це дозволяє кодувати в стилі модуля мінус рекурсії , і він просто працює .
пор. відповідна відповідь .
Якщо під "лінивою оцінкою" ви маєте на увазі, як у комбінованих булей, як у
if (ConditionA && ConditionB) ...
то відповідь просто в тому, що чим менше циклів процесора споживає програма, тим швидше вона запуститься ... і якщо шматок інструкцій з обробки не матиме впливу на результат програми, то це не потрібно, (і, отже, марно. часу) виконувати їх все одно ...
якщо отох, ви маєте на увазі те, що я знав як "ледачі ініціалізатори", як у:
class Employee
{
private int supervisorId;
private Employee supervisor;
public Employee(int employeeId)
{
// code to call database and fetch employee record, and
// populate all private data fields, EXCEPT supervisor
}
public Employee Supervisor
{
get
{
return supervisor?? (supervisor = new Employee(supervisorId));
}
}
}
Ну, ця методика дозволяє клієнтському коду, що використовує клас, щоб уникнути необхідності викликати базу даних для запису даних Supervisor, за винятком випадків, коли клієнт, що використовує об'єкт Employee, вимагає доступу до даних керівника ... це робить процес інстанціфікації працівника швидшим, і все ж, коли вам потрібен Супервізор, перший виклик властивості Supervisor викликає виклик Бази даних, і дані будуть отримані та доступні ...
Витяг з функцій вищого порядку
Давайте знайдемо найбільше число під 100 000, яке ділиться на 3829. Для цього ми просто відфільтруємо набір можливостей, в яких ми знаємо, що рішення лежить.
largestDivisible :: (Integral a) => a
largestDivisible = head (filter p [100000,99999..])
where p x = x `mod` 3829 == 0
Спочатку ми робимо список усіх чисел, що не перевищують 100 000, у зменшенні. Потім ми фільтруємо його за нашим присудком, і тому, що числа сортуються у порядку зменшення, найбільше число, що задовольняє нашому присудку, є першим елементом списку відфільтрованих. Нам навіть не потрібно було використовувати обмежений список для нашого стартового набору. Ось знову лінь у дії. Оскільки ми лише використовуємо голову відфільтрованого списку, не має значення, чи відфільтрований список є кінцевим чи нескінченним. Оцінка припиняється, коли знайдено перше адекватне рішення.