10 нояб. 2011 г.

Исключения из исключений

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

За прошлый разговор с ним мы выбрали язык программирования (Javascript), определили задачу (преобразовать английское существительное из единственного числа во множественное), сделали простейшую систему тестирования (собрали небольшую тестовую базу вопросов-ответов из интернет-тестов) и начали реализовывать функцию, решающую задачу (пока эта функция просто добавляет «s» к любому слову).

На второй разговор школьник внезапно пришёл не один — с ним был друг из гуманитарного класса. Он не столько хотел научиться программировать, сколько ему было интересно поучаствовать в чём-то, связанном с английским языком. Это было прекрасно, потому что при обсуждении логики будущей функции очень важно учесть все возможные языковые нюансы.

Что же они сделали? Не очень много, но этим хочется поделиться:

1. Расширили тестовую базу в полтора раза (взяли примеры из википедии). В процессе этого расширения осознали, что надо слегка переделать тестовую систему, так как бывают слова, имеющие более одной формы множественного числа (например, можно писать и «volcanoes», и «volcanos»), а сейчас это никак не поддержано. Новая версия содержит 59 вопросов (и ответы на 27 из них считаются неправильными).

2. Гуманитарий рассказал, что о множественном числе существительного имеет смысл говорить, если это существительное является исчисляемым. Более того, невозможно без контекста определить, с исчисляемым ли существительным мы имеем дело. Например, «coffee» обычно считают неисчисляемым. Но если мы говорим не о кофе вообще, а о чашках кофе, то вполне можно говорить «two coffees» (как и «two cups of coffee»). Поэтому стоит добавить вывод предупреждения хотя бы для распространённых неисчисляемых существительных. Два заинтересованных человека показали высокую самостоятельность — они не только нашли в интернете список распространённых неисчисляемых существительных, но и написали функцию isUncountable(), определяющую, является ли существительное неисчисляемым:
function isUncountable(iNoun) {
    
var aCommonUncountable = new Array(
        
"water", "tea", "coffee", "milk", // Liquids
        "air", "oxygen", "hydrogen", "nitrogen" // Gases
        // etc
    );
    
for (var i = 0; i < aCommonUncountable.length; i++)
        
if (aCommonUncountable[i] == iNoun)
            
return true;
    
return false;
}

(конечно, ребята подобрали гораздо больше примеров и добавили простейшую проверку правильности функции)

3. Ещё они очень захотели добавить несколько новых простых правил к нашей функции getPlural(). Например, надо научиться определять, оканчивается ли строка на «ch», «sh», «s», «z» или «x». В таких случаях почти всегда для получения множественного числа надо добавить «es». Реализация этой проверки позволила бы заметно продвинуться в улучшении качества результатов.

Тогда я рассказал ребятам о регулярных выражениях. Конечно, разговор был очень поверхностным. Сначала мы расширили нашу функцию, пользуясь методом substr() (т.е. для каждой строки проверяли, совпадают ли два её последних символа с «ch», совпадает ли её последний символ с «s» и так далее. А потом мы разобрались, как всё это многословное безобразие можно заменить одной строкой регулярного выражения:

function  getPlural(iSingular) {
    
if (/ch$|sh$|x$|s$|z$/.test(iSingular))
        
return iSingular + 'es';
    
return iSingular + 's';
}

С этим дополнением программа стала выдавать неправильный результат на 22 тестах из 59. Стало лучше, но ещё есть куда двигаться, так как осталось ещё несколько правил, которые предстоит поддержать, да и исключения (слова вроде «woman» — «women») надо отдельно обрабатывать.

4. Конечно, ребята остро осознали необходимость интерактивной версии. В комментариях к предыдущей заметке были замечания, что именно с неё, а не с тестовой системы надо начинать. Возможно, тут сказывается моя привычка экономить время там, где его можно зря не терять (именно из-за этого я предпочитаю один раз сделать тестирующий код, а не дёргать постоянно интерактивную версию «руками»)... В любом случае, мы пошли этим путём (сначала система тестов, а уже потом приложение для пользователя), что не помешало ребятам попробовать самостоятельно учиться, начать работать с регулярными выраженими и лучше понять тонкости английского языка. Но рано или поздно надо делать пользовательскую версию, что мы и реализовали.

В прошлой заметке мы убедились, что нарисовать табличку 2 на 2 клетки очень легко, поэтому сейчас мы заполним эти клетки следующим образом

Singular: [поле ввода]
Plural: [результат работы функции]
Сделаем это так (сначала идёт реализация функции, возвращающей множественное число, потом вспомогательная функция для взаимодействия с пользователем, далее HTML-код таблицы с формой):
<script type="text/javascript">

function getPlural(iSingular) {
  
if (/ch$|sh$|x$|s$|z$/.test(iSingular))
    
return iSingular + 'es';
  
return iSingular + 's';
}

function writePlural() {
  document.getElementById(
"plural").innerHTML = getPlural(document.getElementById("singular").value.toLowerCase());
}

</script>

<table border=0>
  
<tr><td>Singular:</td><td><input id="singular" onkeyup="writePlural()" value="test"></td></tr>
  
<tr><td>Plural:</td><td><div id="plural">tests</div></td></tr>
</table>

(сохраните этот код в файл plural.html, после чего откройте браузером, чтобы проверить, как всё работает)

Как видите, в верхней правой ячейке таблицы мы поместили поле ввода (тэг input), из которого при изменении содержимого (на самом деле, при отпускании кнопок клавиатуры, так как используется обработчик onkeyup) вызывается наша новая функция writePlural(). Эта функция устроена очень просто — в нижнюю правую ячейку она записывает результат работы нашей функции getPlural(). Единственная тонкость — мы добавили перевод всех символов в нижний регистр (вызываем метод toLowerCase()), так как функция getPlural() пока не готова только к словам, записанным заглавными буквами.

Интерактивная версия очень пригодилась при погружении в регулярные выражения. Многим они кажутся простыми и естественными, но ребятам понадобилось много времени и самостоятельных экспериментов, чтобы осознать, что и как работает.

На этом мы и остановились. Решили, что в следующий раз надо будет сделать следующее:
1) поддержать механизм слов-исключений в функции getPlural() («woman» — «women»),
2) добавить остальные правила построения множественного числа по единственному,
3) расширить тестирующий код для поддержки нескольких правильных ответов («volcanoes», и «volcanos»),
4) решить, как лучше всего использовать результат функции, определяющей, является ли существительное неисчисляемым (выводить подсказку/предупреждение?).

А как бы вы решали эту же задачу со школьниками?

Хорошего дня!

2 комментария:

  1. Поискал решение на cpan - половина кода из Lingua::EN::Inflect это массивы, поэтому им еще писать и писать )
    У меня нет педагогического образования и может я что-то не понимаю, но школьникам должно наскучить максимум через неделю килобайты исключений набивать.

    ОтветитьУдалить
  2. Андрей, Вы правы, если ставить цель создать промышленную библиотеку, то надо сразу забыть об удовольствии от процесса, что, конечно, быстро надоедает. Поэтому мы не доводили код до совершенства (недостижимого на нашем уровне), а изучали разные подходы, пока они радовали.

    Скоро будет продолжение этой истории.

    ОтветитьУдалить