Современная электронная библиотека ModernLib.Net

Программирование на языке пролог

ModernLib.Net / Программирование / Клоксин У. / Программирование на языке пролог - Чтение (стр. 10)
Автор: Клоксин У.
Жанр: Программирование

 

 


В предположении, что выражение не содержит скобок, буква у указывает, что соответствующий ей аргумент может содержать операторы с приоритетом, равным приоритету данного оператора или с более низким приоритетом. Напротив, буква хуказывает, что каждый оператор в соответствующем ей аргументе должен иметь строго более низкий приоритет по сравнению с приоритетом данного оператора. Рассмотрим, что это значит для оператора +, объявленного как yfx. Если имеется выражение

а + b + с

то для него возможны две следующие интерпретации:

(а + b) + с      а + (b + с)

Вторая интерпретация исключается, так как при этом аргумент, стоящий после первого вхождения +, содержит оператор с тем же самым приоритетом (второй оператор +). Это противоречит тому, что в спецификации оператора + после fстоит х.

Оператор, имеющий спецификацию yfx, является левоассоциативным. Аналогично оператор со спецификацией xfyявляется правоассоциативным. Если мы знаем необходимую ассоциативность объявляемого инфиксного оператора, то это значит, что однозначно определяется соответствующая оператору спецификация.

Заметим, что использование букв хи ув двух других случаях имеет тот же смысл относительно того, какие операторы могут появляться в соответствующей позиции при отсутствии скобок. Это значит, что, например, последовательность

not not a

допустима синтаксически, если оператор notобъявлен как fy, и недопустима в случае, когда он объявлен как fx.

Если мы хотим объявить на Прологе оператор с заданными позицией, приоритетом и ассоциативностью таким образом, чтобы Пролог распознавал его при вводе и выводе термов, то мы используем встроенный предикат ор.Если Имя- это оператор, который мы желаем иметь (атом, который мы хотим сделать оператором), Приоритет- приоритет оператора (целое число в соответствующем диапазоне) и Спецификация- спецификация, определяющая положение и ассоциативность оператора (один из приведенных выше атомов), то такой оператор может быть объявлен с помощью выполнения следующего целевого утверждения


?- ор (Приоритет,Спецификация,Имя).


Если объявление оператора является допустимым, то эта цель будет достигнута.

В качестве примера объявления операторов далее приводится полный список базовых операторов, обсуждаемых в данной книге. Конкретные реализации Пролога могут иметь несколько отличный набор «стандартных» операторов; может потребоваться масштабирование указанных приоритетов. Однако взаимный порядок операторов в иерархии приоритетов обычно остается неизменным.


?-op(255,xfx,':-').

?-op(255,fx,'?-').

?-op(254,xfy,';').

?-op(253,xfy,',').

?-op(250,fx,spy).

?-op(250,fx,nospy).

?-op(60,fx,not).

?-op(51,xfy,'.').

?-op(40,xfx,is).

?-op(40,xfx,' =..').

?-op(40,xfx,=).

?-op(40,xfx,\=).

?-op(40,xfx,‹).

?-op(40,xfx,=‹).

?-op(40,xfx,›=).

?-op(40,xfx,›).

?-op(40,xfx, ==).

?-op(40,xfx,\==).

?-op(31,yfx,-).

?-op(31,yfx,+).

?-op(21,yfx,/).

?-op(21,yfx,*).

?-op(ll,xfx, mod).

ГЛАВА 6. ВСТРОЕННЫЕ ПРЕДИКАТЫ

В этой главе будут описаны некоторые встроенныепредикаты, которые может обеспечивать Пролог-система. Что имеется в виду, когда мы говорим, что предикат является встроенным? Это значит, что определение этого предиката уже имеется в Пролог-системе и нет необходимости иметь собственное его описание. Встроенные предикаты предоставляют возможности, которые нельзя реализовать с помощью описаний на чистом Прологе. Они также могут предоставлять удобные средства, избавляя программиста от необходимости самому определять эти предикаты. В действительности мы уже встречались с некоторыми встроенными предикатами – это предикаты для ввода и вывода, обсуждавшиеся в гл. 5. Оператор «отсечения» тоже можно рассматривать как встроенный предикат.

Предикаты для ввода-вывода показывают, что встроенные предикаты могут иметь «побочные эффекты». Это значит, что при доказательстве согласованности целевого утверждения, содержащего такой предикат, помимо конкретизации аргументов предиката могут возникнуть дополнительные изменения. Это, естественно, не может случиться с предикатами, определенными на чистом Прологе. Другой важный факт, касающийся встроенных предикатов, состоит в том, что они могут быть определены только для аргументов конкретного вида. Например, рассмотрим предикат '‹' определенный таким образом, что Х‹Yвыполняется, если число Xменьше, чем число Y. Подобное отношение не может быть определено в Прологе без помощи посторонних средств, использующих некоторые знания о числах. Таким образом,  – это встроенный предикат, а его определение использует некоторые операции вычислительной машины, на которой реализована Пролог-система, для определения относительной величины чисел (представленных в виде двоичного кода или каким-либо иным способом).

Что произойдет, если мы используем в качестве целевого утверждения предикат X‹Y, где Xявляется атомом или даже более того, если как X, так и Yнеконкретизированы? Определение предиката, данное на машинном языке, окажется просто неприменимым. Поэтому мы должны оговорить, что предикат X‹Yможет быть использован в качестве целевого утверждения, если на момент, когда делается попытка выполнить его, обе переменные Xи Yимеют в качестве значений числа. Что произойдет в случае, когда это условие не выполняется, зависит от конкретной реализации Пролог-системы. Возможно, доказательство согласованности такого целевого утверждения просто закончится неудачей. А может быть, будет напечатано сообщение об ошибке и система выполнит ряд действий, соответствующих этой ситуации (подобных прекращению попыток ответить на текущий вопрос).

6.1. Ввод новых утверждений

Когда вы пишете программу на Прологе, вам следует сообщить системе, какие утверждения нужно использовать, и затем задавать относительно них вопросы. Возможно, вы захотите вводить утверждения с терминала или захотите указать Пролог-системе приготовленный вами заранее файл, откуда следует брать утверждения. В действительности обе эти операции с точки зрения Пролога одинаковы, так как терминал рассматривается как файл, имеющий имя user. Имеются два основных встроенных предиката для ввода новых утверждений: consultи reconsult.Кроме того, в языке существует удобная форма записи на случай, когда вы захотите ввести утверждения сразу из нескольких файлов,- так называемая списковая форма записи. Для тех, кому это интересно, укажем, что в разд. 7.13 приведены простые определения на Прологе предикатов consultи reconsult.

consult(X)

Встроенный предикат consultпредназначен для использования в тех ситуациях, когда вы хотите добавить утверждения из некоторого файла /или вводимые с терминала) к утверждениям, уже имеющимся в базе данных. Аргумент предиката должен быть атомом, указывающим имя файла, из которого должны браться утверждения. Какие литеры составляют допустимое имя файла, естественно, зависит от конкретной, используемой вами ЭВМ. Ниже приведены примеры возможной записи целевого утверждения с предикатом consultна различных ЭВМ:


?- consult(myfile).

?- consult('/us/gris/pl/chat').

?- consult('lib:iorout.pl'),


В случае когда Пролог должен доказать согласованность целевого утверждения consult, он просматривает файл, добавляя утверждения, которые он обнаруживает, в конец базы данных. В результате новые утверждения будут помещены после всех уже имевшихся утверждений для тех же предикатов. Если в файле будет обнаружен вопрос, то он будет рассматриваться точно так же, как обычный вопрос, за тем исключением, что ответ не будет напечатан. Обычно не имеет смысла вставлять вопросы в файл с новыми утверждениями, за исключением тех случаев, когда необходимо сделать операции, подобные объявлению новых операторов и печати полезных сообщений.

reconsult(X)

Предикат reconsultаналогичен предикату consult, за исключением того, что вводимые утверждения заменяютвсе имеющиеся утверждения для того же самого предиката. Вследствие этого reconsult удобно использовать для исправления ошибок в программе. Если вы вводите некоторые файлы с утверждениями и затем обнаруживаете, что одно из утверждений содержит ошибку, то вы можете исправить ее, и для этого нет необходимости вводить заново все файлы. Чтобы сделать это, вам необходимо ввести с помощью reconsultфайл, содержащий совокупность правильных утверждений для предиката, содержащего ошибку. Вы можете ввести исправления либо непосредственно с терминала (reconsult (user)), либо сначала отредактировать соответствующий файл, не выходя из Пролог-системы (такую возможность предоставляют не все реализации), и затем ввести этот файл с помощью reconsult. Конечно, при вводе исправленных утверждений с терминала содержание базы данных,с которой работает Пролог, изменится, но при этом исходный файл,содержащий первоначальные утверждения с ошибками, останется неизменным! В разд. 8.5 показано использование предикатов consultи reconsultв процессе разработки программы.

Списковая форма записи

Пролог предоставляет специальную форму записи, позволяющую более удобно задавать предикаты consultи reconsultв качестве целевых утверждений, особенно в случае, когда вы хотите ввести более чем один файл. Для этого достаточно просто записать совокупность имен файлов (как атомов Пролога) в виде списка и задать этот список в качестве целевого утверждения. Если вы хотите, чтобы содержимое файла было просто добавлено к базе данных (consult), то имя такого файла записывается в списке в том виде, как оно есть, если же вы хотите, чтобы при этом произошла замена уже имеющихся одинаковых предикатов (reconsult),то перед именем файла ставится знак '-' (минус). Так, например, вопрос


?- [файл1,-файл2,'фред.1',-'билл.2'].


полностью эквивалентен следующему, но более длинному;


?- consult(файл1), reconsult(файл2), consult('фред:1'), reconsult('билл.2').


Списковая форма сводится к удобству записи, она не дает каких-либо дополнительных возможностей по сравнению с использованием предикатов consultи reconsult.Некоторые реализации Пролога могут использовать в списковой форме записи вместо знака '-' какой-нибудь иной знак, но эффект при этом останется прежним.

6.2. Выполнение и невыполнение целевого утверждения

При нормальном выполнении программы на Прологе целевое утверждение считается согласованным (с базой данных), когда это может быть доказано, и несогласованным в случае, когда доказательства найти не удается. В языке имеются два предиката, позволяющих удобно и явным образом определить случаи, когда целевое утверждение считается согласованным и когда несогласованным. Это предикаты trueи fail.

true

Это целевое утверждение всегда согласуется с базой данных. В действительности этот предикат не является необходимым, так как утверждения в базе данных и цели могут быть переупорядочены или перегруппированы так, чтобы избежать использования предиката true.Однако для удобства он входит в состав встроенных предикатов.

fail

Это целевое утверждение никогда не согласуется с базой данных. Имеются две ситуации, когда этот предикат оказывается полезным. Одна из них – это использование комбинации !- fail,которая уже была описана в разд. 4.3. Конъюнкция целевых утверждений


…!, fail


применяется для того, чтобы указать, что если процесс выполнения дошел до этого момента, то можно больше не пытаться доказать (согласовать) данное целевое утверждение.Конъюнкция считается несогласованной благодаря наличию предиката fail,а родительское целевое утверждение не согласуется ввиду того, что использовано отсечение.

Другая ситуация, в которой используется предикат fail,возникает, когда вы хотите явно указать, что для некоторого целевого утверждения нужно перебрать все решения. Возможно, вы захотите напечатать все возможные решения. Например, выполнение целевого утверждения


?- событие (X,Y), phh(Y), fail.


привело бы к печати всех событий, имеющихся в базе данных, рассмотренной в разд. 5.1, выбор событий и печать их краткого содержания выполняют предикаты событие и phh,при этом цель окажется несогласованной с базой данных. Еще одно применение failрассмотрено в разд. 7.13 (в определении предиката retrac-tall).

6.3. Классификация термов

Если вам надо определить предикаты, которые будут использоваться с аргументами различных типов, то полезно иметь возможность выделять в определении предиката ситуации, соответствующие каждому из возможных типов. В простейшем случае может понадобиться применять разные утверждения в зависимости от того, является ли аргумент целым числом или атомом. Разные утверждения могут потребоваться и для конкретизированных и неконкретизированных аргументов. Приводимые ниже предикаты позволяют программисту включить в утверждения эти дополнительные условия.

var(X)

Целевое утверждение var(X)согласуется с базой данных, если на текущий момент Xявляется неконкретизированной переменной. Таким образом, возможен следующий диалог:


?- var(X).

да

?- var(23).

нет

?- X = Y, Y = 23, var(X).

нет


Неконкретизированная переменная может представлять часть некоторой структуры, которая еще не полностью заполнена. Примерами могут служить неотмеченные клетки на доске для игры в крестики-нолики, рассмотренной в разд. 4.3.3, и незаполненные части упорядоченного дерева, представляющего словарь в разд. 7.1. При работе с такими структурами предикат varочень, полезен при определении, являются ли некоторые части структуры уже заполненными или нет. Это может предотвратить «случайную» конкретизацию переменной при попытке анализа ее значения. Например, при работе со словарем, представленным в виде упорядоченного дерева, может потребоваться узнать, имеется ли уже вход для некоторого ключа, не создавая такой вход в случае его отсутствия. При игре в крестики-нолики может возникнуть необходимость определить, занята или нет некоторая клетка. Попытка сопоставить неконкретизированную переменную с « о» или « х» привела бы просто к тому, что соответствующий символ был бы помещен в клетку, соответствующую переменной.

nonvar(X)

Целевое утверждение nonvar(X)согласуется с базой данных, если Xна текущий момент не является неконкретизированной переменной. Предикат nonvarявляется, таким образом, противоположным по отношению к предикату var. Действительно, он может быть определен на Прологе следующим образом:


nonvar(X):- var(X),!, fail.

nonvar(_).


atom(X)

Целевое утверждение atom(X)согласуется с базой данных, если текущее значение Xявляется атомом в смысле языка Пролог. Как следствие возможен следующий диалог:


?- atom(23).

нет

?- atom(apples).

да

?- atom('/us/qris/pl. 123').

да

?- atom("этo строка").

нет

?- atom(X).

нет

?- atom(book(bronte,w_h,X)).

нет


integer(X)

Целевое утверждение integer(X)согласуется с базой данных, если на текущий момент Xобозначает целое число. Этот предикат можно использовать при определении простого предиката для упрощения арифметических выражений, где необходимо знать, является ли выражение целым числом (см., например, разд. 7.12).

atomic(X)

Целевое утверждение atomic(X)согласуется с базой данных, если на текущий момент Xобозначает либо целое число, либо атом. Предикат atomicможет быть определен через предикаты atomи integerследующим образом:


atomic(X):- atom(X).

atomic(X):- integer(X)

6.4. Работа с утверждениями как с термами

Пролог позволяет программисту анализировать и изменять свою программу (т. е. утверждения, которые используются для доказательства согласованности его целевых утверждений). Это непосредственно следует из того, что утверждения можно рассматривать как обычные структуры языка Пролог. В связи с этим в Прологе есть встроенные предикаты, позволяющие программисту следующее:

• Создавать структуру, представляющую утверждения в базе данных.

• Добавлять к базе данных утверждение, представленное заданной структурой.

• Удалять из базы данных утверждение, представленное заданной структурой.

Большинство операций над базой данных могут быть выполнены с помощью этих предикатов и обычных операций языка Пролог для конструирования и декомпозиции структур. В дополнение к приведенным здесь примерам в разд. 7.8 представлены некоторые способы использования данных предикатов для добавления и удаления утверждений.

Прежде чем мы рассмотрим соответствующие предикаты, необходимо понять, как утверждения Пролога могут быть представлены в виде структур. Для простого факта такой структурой является соответствующий ему предикат с аргументами. То есть факт, подобный


нравится(джон,Х)


может рассматриваться как обычная структура с функтором нравится(имеющим два аргумента) и аргументами джони X.С другой стороны, правило можно рассматривать как структуру с главным функтором ':-'(с двумя аргументами). Этот функтор объявлен как инфиксный оператор. Первым аргументом является заголовок утверждения, а вторым – его тело. Так что


нравится(джон, X):- нравится(Х,вино)


есть не что иное, как


':-' (нравится(джон,Х), нравится(Х,вино))


– совершенно обычная структура. Далее, если правило содержит более одного целевого утверждения, то они считаются объединенными в структуры с функтором ',' (с двумя аргументами). Этот предикат также объявлен как инфиксный оператор. Так что


прародитель(Х, Z):- родитель(Х, Y), родитель(Y,Z)


есть в действительности просто


':-'(прародитель(Х,Z),Y(родитель(Х,Y),родитель(Y, Z))


Далее приведены предикаты, позволяющие программисту анализировать и изменять его утверждения.


listing (A)

Выполнение целевого утверждения listing(A),когда значением Аявляется атом, приводит к тому, что все утверждения, предикат которых совпадает с этим атомом, будут записаны в виде термов Пролога в текущий файл вывода. Таким способом вы можете проверить, какие утверждения для некоторого предиката имеются в базе данных на текущий момент. Конкретный формат представления выводимых утверждений зависит от используемой вами реализации Пролог-системы. Заметим, что будут представлены все утверждения, предикат которых совпадает с атомом независимо от того, сколько аргументов они имеют. Использование предиката listingможет помочь вам обнаружить ошибки в программе. Так, в приведенном далее примере сеанса работы с системой программист обнаруживает, что он неправильно определил предикат обр.


?- [test].

test consulted

да

?- обр([a,b,c,d],X).

нет

?- listing(обр).

обр([],[]).

обр ([_44|_45],_38):-обр(_45,_47),присоединить(_47,[_44],_38).

да


Печать утверждений предиката обрпоказывает, что атом присоединитьнаписан в программе с ошибкой.

clause(X, Y)

Выполнение целевого утверждения вида clause(X, Y)приводит к тому, что Xи Yсопоставляются с заголовком и телом некоторого имеющегося в базе данных утверждения. При попытке выполнить указанное целевое утверждение переменная Xдолжна быть до такой степени конкретизирована, чтобы был известен главный предикат утверждения. Если для данного предиката нет утверждений, то доказательство согласованности целевого утверждения заканчивается неудачей. Если имеется несколько утверждений, соответствующих предикату, то Пролог выбирает первое из них. В этом случае если предпринимается попытка вновь согласовать целевое утверждение, то будет выбрано следующее утверждение и так далее.

Заметим, что в то время как предикат clauseвсегда имеет аргумент, соответствующий телу утверждения, далеко не каждое утверждение действительно имеет тело. Если утверждение не имеет тела, то считается, что оно имеет фиктивное тело true.Мы называли такие утверждения «фактами». В той или иной степени конкретизируя Xи Y, можно искать либо все утверждения, соответствующие данному предикату с вполне определенным числом аргументов, либо все утверждения, соответствующие некоторому образцу. Так, например:


присоединить([], X, X).

присоединить([А|В]),С,[А|D]:- присоединить(В,С,D).

?- clause(присоединить(А,В,С),Y).

А=[], В =_23, С=_23, Y = true;

А = [_23|_24], В =_25, С = [_23|_26], Y = присоединить(_24,_25,_26);

нет


Предикат clauseочень полезен в том случае, если нам надо создать программы, анализирующие или исполняющие другие программы (см. разд. 7.13).

asserta(X), assertz(X)

Два встроенных предиката assertaи assertzпозволяют добавлять новые утверждения в базу данных. Оба предиката действуют в точности одинаковым образом, за тем исключением, что assertaдобавляет утверждение в началобазы данных, в то время как assertzдобавляет утверждение в ее конец.Это отличие можно легко запомнить, учитывая, что « а» является первой буквой английского алфавита, a « z» его последняя буква. При выполнении целевого утверждения asserta(X), Xдолжно иметь значением нечто, что можно представлять как утверждение; действительно, как и в случае clause, Xдолжно быть достаточно конкретизировано, чтобы можно было установить главный предикат. Необходимо подчеркнуть, что результат добавления в базу данных утверждения не устраняется при выполнении возврата. Следовательно, если мы использовали предикат assertaили assertzдля того, чтобы добавить новое утверждение, то это утверждение может быть удалено только в случае, если мы явно укажем это (используя предикат retract).

retract(X)

Встроенный предикат retractпозволяет удалять утверждения из базы данных. Этот предикат имеет один аргумент, представляющий терм, с которым должно быть сопоставлено удаляемое утверждение. Указанный терм должен быть достаточно конкретизирован, чтобы можно было определить предикат утверждения (аналогично предикатам asserta, clauseи т. д.). При попытке выполнить целевое утверждение retract(X)находится первое утверждение в базе данных, с которым может быть сопоставлен X, и это утверждение удаляется. При попытке вновь выполнить это целевое утверждение Пролог просматривает базу данных, начиная с места удаленного утверждения, в попытке найти другое сопоставимое утверждение. Если такое утверждение находится, то выполняются действия, рассмотренные выше. Если делается новая попытка согласовать целевое утверждение, то продолжается поиск следующего подходящего утверждения. И так далее. Заметим, что если утверждение было удалено, то оно ни при каких условиях не будет восстановлено вновь, даже при попытке вновь выполнить предикат retractпри возврате. Если в некоторый момент поиск не дает новых сопоставлений утверждений, то согласование целевого утверждения заканчивается неудачей.

Так как аргумент Xсопоставляется с удаляемым утверждением, то имеется возможность получить точное представление об удаляемом утверждении, даже если Xисходно означал некоторую структуру, содержащую множество неконкретизированных переменных. Это позволяет использовать предикат retractвместо предиката clause, в ситуации когда найденное утверждение сразу же удаляется. Такая ситуация как раз имеет место в определении предиката генатом (разд. 7.8).

6.5. Создание структур и работа с компонентами структур

Обычно когда мы хотим задать в программе на Прологе операции со структурой определенного вида, то мы делаем это, «упоминая» некоторым образом подобную структуру. Это значит, что если предикат используется для обработки множества структур различного вида, передаваемых ему в качестве аргумента, то обычно мы обеспечиваем отдельное утверждение для каждого класса структур. Хорошим примером такого подхода является программа для символьного дифференцирования, которая будет рассмотрена в разд. 7.1. В этой программе используются отдельные утверждения для функторов +, -, * и так далее. Мы знаем заранее, какие структуры могут появиться, и обеспечиваем утверждения для каждой из них.

В некоторых программах мы не можем предвидеть заранее все возможные структуры. Это имеет место, например, при написании программы «красивой печати», которая могла бы печатать произвольные структуры языка Пролог, размещая их в нескольких строках и используя отступы. (См. разд. 5.1, где представлена такая программа для печати списков.) Так, например, возможно, мы захотели бы напечатать терм

книга(629,автор(бронте, эмили),вх)

следующим образом:

книга

 629

 автор

  бронте

  эмили

 вх

Важным моментом является то, что мы хотим, чтобы эта программа работала правильно, какую бы структуру мы ей ни задали. Понятно, что одна из возможностей сделать это – обеспечить отдельное утверждение для каждого функтора, какой только можно представить. Но это работа, которую мы никогда не завершим, потому что существует бесконечно много различных функторов! Написать подобную программу можно, используя встроенные предикаты для работы со структурами произвольного вида. Здесь мы опишем некоторые из них – это предикаты functor, argи ' =..'. Мы опишем также предикат name, выполняющий операции над атомами.

functor(T,F,N)

Предикат functorопределен таким образом, что functor(T,F,N)означает, что Тэто структура с функтором F, имеющим N аргументов.Этот предикат можно использовать двумя основными способами, В первом случае аргумент Туже имеет значение. Целевое утверждение считается несогласованным с базой данных, если Тне является ни атомом, ни структурой. Если Т– это атом или структура, то Fсопоставляется с функтором этой структуры, а Nприсваивается значение, равное числу аргументов функтора. Заметим, что в данном контексте считается, что атом – это структура с числом аргументов 0. Ниже приведено несколько примеров целевых утверждений с предикатом functor:


?- functor(f(a,b,g(Z)),F,N).

Z = _23, F = f ,N =3

?- functor(a+b,F,N).

F = +, N = 2

?- functor([a,b,c],F,N).

F =., N = 2

?- functor(apple,F,N).

F = apple, N = 0

?- functor([a,b,c],'.',3).

нет

?- functor([a,b,c],a,Z).

нет


Прежде чем перейти к обсуждению предиката arg,следует рассмотреть второй способ использования предиката functor.В этом случае первый аргумент целевого утверждения functor (Т, F, N)неконкретизирован. В этом случае два других аргумента должны быть конкретизированы, однозначно определяя функтор и число аргументов соответственно. Целевое утверждение такого вида всегда согласуется с базой данных, и в результате значением Тстановится структура с указанными функтором и числом аргументов. Таким образом, это некоторый способ созданияпроизвольных структур по заданным функтору структуры и числу ее аргументов. Аргументами такой структуры, созданной с помощью предиката functor,являются неконкретизированные переменные. Следовательно, эта структура будет сопоставима с любой другой структурой, имеющей тот же функтор и одинаковое число аргументов.


  • Страницы:
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24