Ваша первая чашечка CoffeeScript: Часть 2. Изучение языка на практических примерах

Этот цикл статей посвящен изучению популярного языка программирования CoffeeScript, который построен поверх JavaScript. В первой части мы рассмотрели преимущества языка для разработчиков, установили компилятор CoffeeScript и использовали его для создания кода, который может работать в Web-браузере или на сервере. В этой статье мы займемся CoffeeScript более основательно. Мы решим несколько задач программирования с математическим уклоном из Проекта Эйлера. Исходный код примеров прилагается.

Майкл Галпин, инженер по программному обеспечению, Vitria Technology

Майкл Галпин (Michael Galpin) имеет учёную степень по математике в Калифорнийском Технологическом институте. Он является Java-разработчиком с конца 90-х гг. и работает инженером по программному обеспечению в Vitria Technology, в Саннивейл, Калифорния.



18.07.2012

Введение

Этот цикл статей посвящен изучению CoffeeScript, нового языка программирования, который построен поверх JavaScript и обеспечивает рациональный синтаксис. Код CoffeeScript компилируется в эффективный код JavaScript. Его можно использовать не только для выполнения JavaScript в Web-браузере, но и с такими технологиями, как Node.js для серверных приложений. В первой части мы установили компилятор CoffeeScript и использовали его для создания кода, который может работать в Web-браузере или на сервере.

Теперь приступим к изучению языка CoffeeScript на примерах задач программирования из Проекта Эйлера (см. раздел Ресурсы). В этих примерах используются функции, диапазоны, выделения (comprehensions), блочные операторы, массивы и некоторые аспекты CoffeeScript как объектно-ориентированного языка.

Загрузите исходный код, который будет использоваться в этой статье.


Функции, диапазоны и выделения

Первая задача из проекта Эйлера, которую мы попытаемся решить, это задача 6 (см. раздел Ресурсов), в которой требуется вычислить сумму квадратов первых натуральных чисел, квадрат суммы этих же чисел, а затем определить разность. Для решения этой задачи будет использоваться CoffeeScript Read-Evaluate-Print-Loop (REPL). В листинге 1 показан код и соответствующий результат выполнения REPL.

Листинг 1. Задача 6 в REPL
coffee> square = (x) -> x*x
[Function]
coffee> sum = (nums) -> nums.reduce (a,b) -> a+b
[Function]
coffee> diff = (nums) -> (square sum nums) - (sum nums.map square)
[Function]
coffee> diff [1..100]
25164150

Разбор

  1. В REPL определяем функцию. (Как говорилось в Части 1, CoffeeScript заимствует природу функционального программирования JavaScript, отбрасывая большую часть C-подобного синтаксиса JavaScript, который делает язык слишком громоздким с точки зрения элегантного функционального программирования.)

    В примере из листинга 1 определяется функция square, которая принимает один параметр и возвращает произведение этого параметра на самого себя (его квадрат). Затем REPL сообщает, что вы определили функцию.

  2. Определяем другую функцию, sum, которая принимает один параметр: массив. Для этого массива sum вызывает метод reduce.

    Метод reduce не добавлен CoffeeScript, а входит в состав самого JavaScript (он появился в JavaScript 1.8). Этот метод подобен функции reduce языка Python или функции fold языка Haskell или Scala. Он принимает функцию и перебирает массив слева направо, применяя функцию к предыдущему полученному значению и следующему значению из массива. Благодаря компактному синтаксису CoffeeScript функция reduce проста в применении. В данном случае она описывается выражением (a,b) -> а + b. Это функция принимает два значения и складывает их, так что все элементы массива складываются друг с другом.

  3. Создаем функцию diff, которая принимает массив чисел, вычисляет два подвыражения, а затем вычитает одно из другого. Первое подвыражение перебирает массив, суммируя его элементы, а затем возводит результат в квадрат.

    Во многих случаях CoffeeScript позволяет опустить скобки во избежание всяческой путаницы. Например, запись square sum nums эквивалентна square(sum(nums)). Второе подвыражение применяет к массиву метод map, еще один метод из JavaScript 1.8, который принимает на вход другую функцию. Он применяет эту функцию к каждому элементу массива, создавая новый массив результатов. В примере из листинга 1 в качестве входного параметра для map используется функция возведения в квадрат, что приводит к созданию массива, элементы которого являются квадратами элементов входного массива. Затем мы просто передаем их функции sum для получения суммы квадратов.

  4. Передаем массив чисел функции diff для решения задачи 6 с использованием диапазона [1..100].

    Этот диапазон эквивалентен массиву всех чисел от 1 до 100 включительно. Если нужно, чтобы он был исключающим, достаточно написать [1...100] - с тремя точками вместо двух. Передавая это функции diff, получаем решение задачи 6.

Теперь вернемся немного назад и рассмотрим задачу Проекта Эйлера 1 (см. раздел Ресурсы), где нужно просуммировать все целые числа до 1000, кратные 3 или 5. Пожалуй, это самая простая задача Проекта Эйлера. Ее легко решить с применением функций и диапазонов, как в задаче 6. Однако применение функции выделения CoffeeScript дает более элегантное решение, показанное в листинге 2.

Листинг 2. Решение задачи 1 с использованием функции выделения
coffee> (n for n in [1..999] when n % 3 
== 0 or n % 5 == 0).reduce (x,y) -> x+y
233168

Всегда приятно решить задачу одной строкой кода, а лаконичный синтаксис CoffeeScript помогает в этом. В решении, приведенном в листинге 2, с помощью выделения составляется список всех целых чисел, кратных 3 или 5. Из диапазона [1..999] выбираются только те значения, которые делятся на 3 или 5. Затем используется еще одна операция reduce для суммирования значений. REPL вычисляет эту строку кода и распечатывает решение.

В следующем разделе рассматриваются более сложные задачи и производится дальнейшее исследование CoffeeScript.


Блочные операторы, массивы и файлы

В задаче 4 Проекта Эйлера (см. раздел Ресурсы) требуется найти самый большой палиндром, полученный произведением двух трехзначных чисел. Существует много способов решения этой задачи; один из них показан в листинге 3.

Листинг 3. Проверка на палиндромы
isPal = (n) -> 
  digits = [] 
  while n > 0 
    digits.push(n % 10) 
    n = Math.floor(n / 10) 
  len = digits.length 
  comps = [0..len/2].map (i) -> digits[i] == digits[len-i-1] 
  comps.reduce (a,b)-> a && b 

vals = []
vals.push(x*y) for x in [100..999] for y in [100..999]
pals = vals.filter (n) -> isPal(n)
biggest = pals.reduce (a,b) -> Math.max(a,b)
console.log biggest
  1. Определим функцию isPal, которая проверяет, является ли число палиндром. Эта функция несколько сложнее, чем те, что мы определяли до сих пор. Она состоит из семи строк кода.

    Вы, наверное, заметили, что в CoffeeScript не используются фигурные скобки ({}) или какой-либо другой явный механизм для обозначения начала и конца функции. В нем применяются пробелы (отступы), как в Python, и все функции начинаются одинаково: со списка параметров, оканчивающегося стрелкой (->). Затем мы создаем пустой массив для цифр числа и начинаем цикл while. Этот цикл схож с циклом while JavaScript (или C, Java и т.п.). Предикат (n > 0) не нужно заключать в круглые скобки. Тело цикла смещается вправо, что указывает на то, что это часть цикла. Внутри цикла мы отделяем от числа последнюю цифру, перемещаем ее к началу массива и делим число на 10, отбрасывая остаток. Это приводит к созданию массива цифр исходного числа. Цикл можно заменить просто выражением digits = new String n, которое преобразует n в строку. Остальная часть кода работает как есть.

  2. Получив массив цифр, мы создаем массив в половину длины этого массива. Функция map превращает каждый элемент массива в логическое значение, указывающее, равны ли цифры, равноудаленные от начала и от конца массива.

    Если да, то это палиндром. Чтобы проверить результат, используем другую функцию reduce, на этот раз вычисляя логическое произведение.

  3. Теперь, когда у нас определена функция isPal, используем ее для тестирования своих палиндромов. Проверим все числа, которые являются произведением двух трехзначных чисел.
    1. Создадим два выделения, каждое из которых начинается с наименьшего трехзначного числа (100) и заканчивается наибольшим (999).
    2. Для каждого выделения вычислим произведение и поместим его в массив.
    3. Воспользуемся другой функцией reduce, чтобы найти наибольший элемент массива. Наконец, распечатаем этот элемент с помощью console.log.
    4. Сохраним его в файле.

В листинге 4 показано, как выполнить код и зафиксировать время решения.

Листинг 4. Решение задачи 4
$ time coffee p4.coffee
906609

real	0m2.132s
user	0m2.106s
sys	0m0.022s

Сценарий из листинга 4 выполняется на быстром компьютере более двух секунд, во многом за счет сложного выделения, которое создает 899*899 = 808 201 проверяемых значений (многие из них - дубликаты). В качестве дополнительного упражнения код листинга 3 можно оптимизировать.(Подсказка: преобразование числа в строку выполняется намного быстрее.)

Задача 22 Проекта Эйлера (см. раздел Ресурсы) требует выполнения сложных вычислений над списком строк. Список считывается из файла, анализируется, сортируется, и каждая строка преобразуется в число, а затем суммируются произведения всех чисел на номера их позиции в списке. Задача 22 позволяет увидеть, как работают файлы в CoffeeScript. В ней применяются также некоторые манипуляции со строками и дополнительные трюки с массивами. Решение приведено в листинге 5.

Листинг 5. Работа с файлами в CoffeeScript
path = "/path/on/your/computer/names.txt"
fs = require "fs"
contents = fs.readFileSync path, "utf8"
strs = contents.split(",")
names = strs.map (str) -> str[1 .. str.length - 2]
names = names.sort()
vals = names.map (name) -> name.split("")
vals = vals.map (list) ->
	list.map (ch) -> 1 + ch.charCodeAt(0) - 'A'.charCodeAt(0)
vals = vals.map (list) ->
	list.reduce (a,b) -> a+b
vals = ((i+1)*vals[i] for i in [0...vals.length])
total = vals.reduce (a,b) -> a+b
console.log total
  1. Сохранте файл. В описании задачи есть ссылка, или можно использовать исходный код, прилагаемый к этой статье. Замените значение переменной path на абсолютный путь к файлу в вашем компьютере.

    CoffeeScript не содержит никаких специальных библиотек для работы с файлами. Вместо этого используется node.js и его модуль fs. Загрузите модуль fs с помощью функции require.

  2. Прочтите содержимое файла с помощью fs.readFileSync. Файл содержит имена типа "MARY", "PATRICIA" и т.п. Он записан в одну строку, так что мы используем метод split для разделения его по запятым.

    В начале и в конце каждой строки остаются кавычки ("). Чтобы избавиться от них, используем функцию map и заменим каждую строку вырезкой. Если str - строка, то str[1 .. str.length -2] - подстрока, которая начинается со второго символа и заканчивая предпоследним. Код будет в точности удалять первый и последний символы – то есть те самые кавычки. Вырезание строк может быть очень полезным.

  3. Получив список строк без кавычек, мы готовы к сортировке. Используем метод sort. Необходимо преобразовать строки в число, заменяя каждый символ номером его позиции в алфавите (A -> 1, B -> 2, C -> 3 и т.д.).
    1. Опять преобразуем каждую строку в массив символов с помощью метода split.
    2. Затем с помощью метода charCodeAt поставим в соответствие каждому символу числовое значение.
    3. Сложим эти числовые значения с помощью еще одной операции reduce.
    Список строк преобразуется в список чисел.
  4. Умножим каждое число на его позицию в списке и просуммируем с помощью другого выделения. Создадим новый массив, каждый элемент которого получается путем умножения значения элемента из предыдущего массива на номер его позиции. Просуммируем элементы этого массива с помощью еще одной операции reduce и распечатаем сумму.

Опять же, можно сохранить результат в файле, а затем выполнить его с измерением времени, как показано в листинге 6.

Листинг 6. Время выполнения задачи 22
$ time coffee p22.coffee
871198282

real	0m0.133s
user	0m0.115s
sys	0m0.013s

Решение задачи 22 занимает менее 0,2 с. Для выполнения необходимых расчетов потребовалось почти столько же строк кода, сколько их в описании хода вычислений на обычном языке. Это хороший пример лаконичного синтаксиса CoffeeScript. Можно предположить, что на других языках программирования для этого примера потребуется намного больше строк кода.

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


Объектно-ориентированный CoffeeScript

Как упоминалось в первой части, основной недостаток JavaScript заключается в его «нестандартном» стиле объектно-ориентированного программирования (OOП). В подавляющем большинстве случаев применяется ООП на основе классов. CoffeeScript обеспечивает ООП, основанный на классах.

Следующая задача состоит в использовании ООП CoffeeScript для решения задачи 35 Проекта Эйлера (см. раздел ресурсы). В задаче 35 говорится, что круговые простые числа — это особый вид простых чисел, для которых все перестановки цифр из конца в начало являются простыми числами. В коде, приведенном в листинге 7, ООП используется для вычисления количества круговых простых чисел меньше миллиона.

Листинг 7. Подсчет круговых простых чисел
class PrimeSieve 
  constructor: (@max) -> 
    @nums = [2..@max]
    for p in @nums 
      d = 2*p 
      while p != 0 and d <= @max 
        @nums[d-2] = 0 
        d += p 
  isPrime: (n) -> @nums[n-2] != 0
  thePrimes: -> @nums.filter (n) -> n != 0

class CircularPrimeGenerator extends PrimeSieve
  genPerms = (num) -> 
    s = new String num 
    x = (for i in [0 ... s.length] 
      s[i+1 ... s.length].concat s[0..i]) 
    x.map (a) -> parseInt(a)
  isCircularPrime : (n) -> 
    perms = genPerms(n)
    len = perms.length
    primePerms = perms.filter (p) => @isPrime(p)
    len == primePerms.length
  theCircularPrimes: ->
    (p for p in @thePrimes() when @isCircularPrime(p))  
	
max = process.argv[2]
generator = new CircularPrimeGenerator max

console.log "Number of circular primes less than #{max} is 
#{generator.theCircularPrimes().length}"
  1. Создаем класс PrimeSieve, который реализует классический алгоритм вычисления всех простых чисел до определенного значения "Решето Эратосфена".

    Знак @ означает свойство класса, а также сокращение выражения 'this.'. Таким образом, запись @nums = [2..@max] эквивалентна this.nums = [2.. this.max].

    Методы класса указаны их именем, за которым следует точка с запятой и определение функции. Первый метод, называемый constructor, - это конструктор класса. К примеру, новый PrimeSieve(100) приведет к вызову конструктора со значением max равным 100, которое присваивается параметру this.max. Наш конструктор строит решето и сохраняет простые числа в переменной экземпляра @nums. Затем объявляются еще два метода: isPrime и thePrimes. Метод thePrimes использует фильтр массива для удаления из @nums составных чисел.

  2. Объявляем подкласс CircularPrimeGenerator класса PrimeSieve. Как и во многих популярных языках ООП, в CoffeeScript используется синтаксис class ... extends. Этот класс будет наследовать конструктор, переменные экземпляров и методы PrimeSieve. Он содержит:
    • метод genPerms для генерации всех круговых перестановок цифр данного числа;
    • метод isCircularPrime, который создает все перестановки данного числа и удаляет из списка перестановок все составные числа. Если отфильтрованный список содержит все те же элементы, что и неотфильтрованный, то это круговое простое число;
    • метод theCircularPrimes, который создает список всех круговых простых чисел с помощью выделения.
    Заметим, что можно использовать метод @thePrimes, определенный в суперклассе, а затем просто отфильтровывать некруговые простые числа.

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

  3. В листинге 7 принимается аргумент командной строки, указывающий максимальное значение для вычисления круговых простых чисел. Все аргументы командной строки можно получить с помощью process.argv. Первые два значения этого массива - это команда и сценарий, так что первый параметр для использования в сценарии содержится в process.argv[2].

    Создадим экземпляр CircularPrimeGenerator, используя максимальное значение, переданное в сценарий.

    распечатаем число найденных круговых простых чисел с помощью console.log.

В этом примере используется еще одна удобная особенность CoffeeScript: интерполяция строк для создания строки, которая передается в console.log.


Заключение

В этой статье рассмотрены многие особенности CoffeeScript. Она кратко описывает синтаксис и особенности языка программирования, которые позволяют изящно реализовать многие распространенные алгоритмы. В то же время CoffeeScript упрощает объектно-ориентированное программирование. С какой бы задачей вы не столкнулись, синтаксис CoffeeScript упростит ее решение.

Следующая статья этого цикла будет более прагматичной и посвящена использованию CoffeeScript для клиентских сценариев Web-приложений.


Загрузка

ОписаниеИмяРазмер
Исходный код примера для статьиcs2.zip19 КБ

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Web-архитектура, Open source
ArticleID=826495
ArticleTitle=Ваша первая чашечка CoffeeScript: Часть 2. Изучение языка на практических примерах
publish-date=07182012