 | Уровень сложности: простой Тед Ньювард, Глава, Neward & Associates
05.05.2008
Платформа Java™ исторически была территорией объектно-ориентированного программирования,
но даже ярые приверженцы языка Java начали обращать внимание на последнюю, вышедшую из тени,
тенденцию в разработке приложений: функциональное программирование.
В этой новой серии Тед Ньювард представляет Scala, язык программирования, объединяющий
функциональную и объектно-ориентированную техники под управлением JVM.
По ходу дела Тед обосновывает, почему следует уделить время изучению Scala — параллелизм
одна из причин — и показывает, насколько быстро это может окупиться.
Вы никогда не забудете свою первую любовь.
В моем случае ее звали Табинда (Бинди) Хан. Это были безмятежные годы моей юности,
седьмой класс, если быть точным, а она была красива, смышлена и, что самое замечательное,
она хохотала над моими неуклюжими подростковыми шуточками. Мы «запали» друг на друга
(как это тогда называлось), со взлетами и падениями в отношениях, длившихся преимущественно
в 7 и 8 классах. Но к 9 классу мы расстались, это было вежливым способом сказать, что ей
надоело на протяжении двух лет выслушивать однообразные и нескладные юношеские хохмы.
Я никогда ее не забуду (не в последнюю очередь потому, что мы столкнулись друг с другом
снова на 10-летней встрече одноклассников), но что более важно — я никогда не расстанусь
с этими заветными (если несколько преувеличить) воспоминаниями.
 |
Об этой серии
Тед Ньювард погружается в язык программирования Scala и берет вас с собой.
В этой новой серии
от developerWorks вы узнаете вокруг чего поднята такая шумиха
и увидите некоторые лингвистические возможности Scala в действии.
Код Scala и код Java будут показаны бок о бок, если таковое сравнение будет важно,
но (как скоро выяснится) многие вещи в Scala не могут быть напрямую соотнесены с
чем-либо таким, что вам знакомо из Java-программирования, но в этом и заключается
основное очарование Scala! В конце концов — если Java-код способен это сделать,
зачем же утруждать себя изучением Scala?
|
|
Java-программирование и объектная ориентированность стали первой любовью для многих программистов,
и мы принимаем это с таким же уважением и полным благоговением, как и мое отношение к Бинди.
Некоторые разработчики скажут вам, что Java-программирование уберегло их от адских мук в
преисподней, порождаемой управлением памятью и C++. Другие скажут, что Java-программирование
возвышает их над пучиной процедурной безысходности. Найдутся даже такие разработчики, для которых
объектно-ориентированное программирование в Java-коде просто является «изначальной данностью
этого мира» (а как же иначе — ведь это работало и при моих предках и при их предках до них самих!)
Однако, время неизбежно преодолевает все первые влюбленности и наступает момент двигаться дальше.
Чувства изменились а участники этой истории стали зрелыми (и, будем надеяться, подучили несколько
новых шуток), Но более значимо — изменился мир вокруг нас. Многие Java-разработчики осознают,
что как бы мы ни любили Java-программирование, пришло время выдвигаться к новым горизонтам на
наших «девелоперских» просторах и выяснить, как мы можем все это постичь.
Я буду любить тебя всегда ...
В течение последних пяти лет обозначилась нарастающая волна недовольства языком Java.
В то время, как кое-кто может указать на развитие Ruby on Rails как на основной фактор в этом
смысле, я приведу доводы что RoR (в нотации, знакомой поклонникам Ruby) является следствием,
а не причиной. Или же, выражаясь точнее, докажу, что факт восприятия Java-разработчиками Ruby
следует из глубинной, более скрытой причины.
Попросту говоря, в программировании на Java проступает возраст.
Или, что будет точнее, возраст проступает у языка Java.
И действительно: когда язык Java впервые появился на свет, Клинтон (в свое первое президентство)
восседал в офисе, а Интернетом регулярно пользовались лишь настоящие энтузиасты, главным образом
потому, что соединение по телефонной линии было единственным доступным способом в домашних условиях.
Блоги (сетевые дневники) еще не были изобретены, и все верили, что наследование является
фундаментальным подходом в повторном использовании. Мы также верили, что объекты являются наилучшим
способом моделирования мира и что Закон Мура будет всегда господствовать экспоненциально.
На самом деле — именно законом Мура были в крайней степени озабочены многие в индустрии.
С 2002/2003 доминирующей тенденцией в микропроцессорных технологиях было создание процессорных
модулей с множеством «ядер»: в сущности, множества процессоров на одном чипе. Это позволяет
обойти Закон Мура, который говорит, что скорость процессоров удваивается каждые 18 месяцев.
Ситуация с многопоточными средами, исполняемыми на двух процессорах одновременно, вместо того,
чтобы выполнять стандартный круговой цикл на едином процессоре, означает, что код должен быть
непробиваемой глыбой с точки зрения потокобезопаности, коль скоро такой код претендует на существование.
В академическом сообществе предпринималось множество исследований касательно этой специфической
проблемы, приведших к изобилию новоиспеченных языков. Существенным недостатком выступал тот факт,
что большинство из этих языков надстраивались над своей собственной виртуальной машиной или
интерпретатором, означая таким образом (как делает и Ruby) переход на новую платформу.
Кризис параллелизма — настоящая головная боль и некоторые из новых языков предлагают мощные
решения, но слишком много корпораций и предприятий помнят миграцию с C++ на платформу Java
каких-то 10 лет тому назад. Перемещение на новую платформу — это риск, который многие компании
даже не собираются рассматривать всерьез. Многие, в действительности, по-прежнему зализывают
раны от последнего перехода на Java-платформу.
Знакомимся со Scala.
SCAlable LAnguage - Масштабируемый язык
 |
Почему Scala?
Изучение нового языка программирования — всегда нелегкая задача,
особенно если язык требует совершенно иного мышления в подходах к решению проблем,
а именно функционального подхода, принятого в Scala. И даже больше, когда речь идет
о скрещивании подходов, наподобие слияния в Scala объектно-ориентированной концепции
с функциональной.
Постижение Scala требует времени, и с таким обязательством, давлеющим
над вашим и без того напряженным планом работ, вы можете, в первом приближении,
вовсе не разглядеть возврата по вложениям. Позвольте мне заверить вас в том, что
Scala располагает целым рядом интригующих возможностей, многие из которых будут
освещены в предстоящих публикациях этой серии.
Ниже приведен неполный список того, что поможет вам увидеть выгоды от освоения
этого языка. Используя Scala, вы сможете:
-
Создавать внутренние DSL —
типа Ruby, благодаря гибкой реализации идентификаторов в Scala.
-
Создавать в наивысшей степени масштабируемые, параллельные обработчики данных, благодаря тому, что Scala
изначально стоит на позициях неизменяемости состояния.
-
Сократить размер эквивалентного Java-кода в половину или на две трети, из-за
обилия в Scala синтаксических приемов, таких как замыкания и неявные определения.
-
Использовать преимущества параллельных аппаратных архитектур (таких как
многоядерные процессоры), т.к. Scala предрасполагает к функциональному дизайну.
-
Контролировать большие объемы кода, из-за упрощения в Scala правил
жесткого типизирования, выставляющих, по существу, одно требование — "все является объектом."
Несомненно — Scala олицетворяет мощный, новый взгляд на программирование; факт
компиляции в код, совместимый для запуска под управлением JVM как нельзя лучше позволяет
воспользоваться Scala для "настоящей работы", да еще с такой легкостью.
|
|
Scala — функционально-объектный гибридный язык с несколькими сильными сторонами,
подогревающими интерес к нему:
-
Во-первых, Scala компилируется в байт-код Java, подразумевая его запуск на JVM.
В дополнение к вашей возможности продолжать использовать все преимущества Java как
развитой экосистемы с открытым кодом, Scala может быть интегрирован в существующее
информационное пространство (среду) с нулевыми усилиями на миграцию.
-
Во-вторых, Scala опирается на функциональные принципы Haskell и ML, не отказываясь от тяжкого
бремени привычных объектно-ориентированных концепций, столь полюбившихся Java-программистам.
В результате Scala может смешивать лучшее из двух миров в единое целое, что предоставляет
значительный выигрыш без жертвования простотой, на которую мы привыкли рассчитывать.
-
И в заключение, Scala был разработан Мартином Одерски, возможно, более известным в
Java-сообществе благодаря языкам Pizza и GJ, последний из которых стал рабочим прототипом
универсальных типов (generics) в Java 5. Раз так, Scala несет ощущение «серьезности»;
этот язык не создавался по капризу и он не будет брошен на произвол.
Как и предполагает название Scala, он также является в высокой степени масштабируемым языком.
Позже я расскажу об этом подробнее, как только мы немного углубимся в эту серию публикаций.
Загрузка и установка Scala
Вы можете загрузить комплект поставки Scala с домашней страницы Scala. Текущим на момент
написания статьи релизом является 2.6.1-final. Он доступен в виде Java-инсталлятора,
пакетов RPM и Debian, архивов gzip/bz2/zip, которые достаточно просто распаковать в
целевую директорию, а также в виде исходного tarball, позволяющего выполнить сборку с нуля.
(Версия 2.5.0-1 доступна для пользователей Debian с Web-сайта Debian в виде готового к
употреблению инсталляционного модуля. Однако версия 2.6 имеет некоторые незначительные
отличия, поэтому рекомендуется загрузка и установка напрямую с сайта Scala.)
Установите Scala в каталог по выбору — я пишу это, находясь в среде Windows®,
поэтому у меня это будет каталог C:/Prg/scala-2.6.1-final. Задайте этот каталог в
переменной окружения SCALA_HOME и добавьте SCALA_HOME\bin
к PATH для упрощения вызова из командной строки.
Для проверки вашей инсталляции просто запустите
"scalac -version".
В ответ должно последовать "Scala version, 2.6.1-final".
Функциональные концепции
Прежде, чем мы начнем, я представлю несколько функциональных концепций, необходимых для понимания
того, почему же Scala ведет себя так, а не иначе. Если вам доводилось иметь дело с функциональными
языками — Haskell, ML или же с недавним пополнением в функциональном мире — F#, вы можете
переходить к следующему разделу.
Функциональные языки получили свое название из концепции, предполагающей, что программы должны
вести себя как математические функции; другими словами, при заданном наборе входных параметров
функция должна всегда возвращать один и тот же результат. Это не просто означает, что каждая
функция должна возвращать значение, а и то, что эти функции не должны переносить никакого
внутреннего состояния от одного вызова к другому. Такая основополагающая идея несохраняемости
состояния, перенесенная в функциональный/объектный мир и означающая объекты, неизменяемые по
своей природе — главная причина того, почему функциональные языки провозглашаются великими
спасителями безумно параллельного мира.
В отличие от многих динамичных языков, с недавних пор начавших отвоевывать себе пространство
на платформе Java, Scala является статически типизированным, как и Java. Однако, в противовес
платформе Java, Scala прилагает существенные усилия к использованию выведения типов
(type inferencing), означающего, что компилятор выполняет глубокий анализ кода для выяснения
типа конкретного значения, без вмешательства программиста. Выведение типа требует меньшей
избыточности в типизации кода. Например, рассмотрим приведенный в листинге 1 Java-код,
необходимый для объявления локальных переменных и присвоения им значений:
Листинг 1. Ох уж этот гениальный javac ...
class BrainDead {
public static void main(String[] args) {
String message = "Зачем указывать javac-у, что message – это строка?" +
"А что же еще это может быть, если я и так присваиваю String?";
}
}
|
Scala не нуждается в таком расписывании, как и будет показано далее.
Множество прочих функциональных особенностей (таких как сопоставление с шаблоном - pattern matching)
проложили себе путь в Scala, но полное их перечисление было бы забеганием вперед.
Scala также добавляет некоторое количество деталей, отсутствующих на данный момент в Java,
скажем, перегрузка оператора - operator overloading (являющаяся, как оказывается,
тем, что Java-программисты вообще не могут себе вообразить), универсальные типы (generics)
с «верхним и нижним ограничениями по типу», виды (views) и многое другое.
Эти особенности, среди прочего, делают Scala чрезвычайно мощным для решения такого рода задач,
как обработка или генерация XML.
Но хватит общих абстрактных рассуждений: программисты любят видеть код,
так давайте же и посмотрим, на что способен Scala.
Приступаем
Наша первая Scala-программа будет стандартной демонстрационной программой,
Hello World, как того требуют Боги Компьютерных Наук:
Листинг 2. Hello.Scala
object HelloWorld {
def main(args: Array[String]): Unit = {
System.out.println("Hello, Scala!")
}
}
|
Скомпилируйте это вызовом scalac Hello.scala и запустите полученный код при помощи либо
запускающего модуля (scala HelloWorld), либо используя традиционный запуск Java, не забыв
включить библиотеку ядра Scala в JVM classpath
(java -classpath %SCALA_HOME%\lib\scala-library.jar;. HelloWorld).
В любом случае должно появиться традиционное приветствие.
Некоторые элементы в листинге 2 определенно вам знакомы, но также задействованы и
некоторые явно новые. К примеру, начав с привычного вызова System.out.println,
Scala демонстрирует свою дружественность к лежащей в основе платформе Java.
Scala преодолевает огромное расстояние, чтобы обеспечить доступность
всей мощи Java в Scala-программах.
(В действительности, позволительно даже наследовать тип Scala от Java-класса и наоборот,
но об этом позже).
С другой стороны, если вы внимательны, вы должно быть заметили отсутствие точки с запятой
в конце вызова System.out.println
— это не опечатка.
В отличие от Java-платформы, Scala не требует точки с запятой для завершения оператора
если это очевидно по факту окончания строки. Тем не менее, точки с запятой по-прежнему
поддерживаются и являются иногда необходимыми если, например, физически в одной и той
же строке присутствует более одного оператора. В большинстве случаев,
прогрессирующие Scala-программисты могут опускать точки с запятой, а компилятор Scala
ненавязчиво напомнит вам (обычно посредством броского сообщения об ошибке),
когда такой разделитель будет необходим.
К тому же, хотя это и второстепенная мелочь, Scala не требует в названии файла, содержащего
определение класса, отражать имя этого класса. Некоторые найдут в этом освежающее отличие
от Java-программирования; а именно те, кто не может без проблем продолжать использовать
принятое в Java класс/файл-соглашение об именовании.
Ну а теперь давайте взглянем на то, в чем Scala действительно начинает отклоняться от
традиционного Java/объектно-ориентированного кода.
Функция и форма — наконец-то вместе
Прежде всего, приверженцы Java отметят, что вместо "class",
HelloWorld определен с использованием ключевого слова object.
Это поклон Scala в сторону вездесущего шаблона Singleton —
служебное слово object сообщает компилятору Scala, что это
будет синглетон-объект, и в результате Scala гарантирует,
что в любой момент времени будет существовать только один
экземпляр HelloWorld. Обратите внимание - по той же причине
main не определяется как статический метод, как это было бы
в Java-программировании. Фактически Scala избегает использования
"static" вообще.
Если же приложению необходимо иметь экземпляры некоторого
типа наряду с его "глобальным" вариантом,
приложение Scala позволит как определение
class так и object для одного и того же имени.
Далее, посмотрите на определение main, которое, как и в случае Java-кода,
является общепринятой точкой входа для Scala-программ.
Это определение, хотя оно и выглядит отличным от такового в Java,
идентично: main принимает массив строк в качестве аргумента и
ничего не возвращает. Тем не менее, в Scala такое определение
несколько отличается от Java-версии. Определение параметра args
задано как args: Array[String].
В Scala массивы представлены экземплярами обобщенного класса Array,
кроме прочего демонстрирующего то, что Scala использует
квадратные скобки ("[]") вместо угловых ("<>") как признак
параметризованных типов. Ну и, для полноты картины, отметим
использование в языке шаблона "имя: тип".
Как и в случае с другими функциональными языками, Scala требует, чтобы функции
(в данном случае метод main) в обязательном порядке возвращали значение.
Вот он и возвращает значение "не-значение", называемое Unit.
В практическом смысле Java-разработчики могут думать о Unit
как об аналоге void, по крайней мере, на данный момент.
Синтаксис для определения метода выглядит весьма интересно,
поскольку использует оператор "=", почти так, как если
бы выполнялось присвоение тела метода, следующего за
идентификатором main. В действительности это именно то,
что имеет место: в функциональном языке функции являются
концепциями первого рода, как переменные и константы,
а, стало быть, и синтаксически интерпретируются как таковые.
Вы сказали замыкания?
Из того, что функции являются концепциями первого рода,
вытекает необходимость представления их каким-либо образом
в виде автономных конструкций, также известных как замыкания (closures) —
понятие, столь горячо обсуждаемое последнее время Java-сообществом.
В Scala это легко выполнимо. Прежде чем демонстрировать возможности замыканий,
рассмотрим простую программу в листинге 3.
Здесь функция oncePerSecond()
повторяет свою логику (в данном случае — печать в System.out)
каждую секунду.
Листинг 3. Timer1.scala
object Timer
{
def oncePerSecond(): Unit =
{
while (true)
{
System.out.println("Time flies when you're having fun(ctionally)...")
Thread.sleep(1000)
}
}
def main(args: Array[String]): Unit =
{
oncePerSecond()
}
}
Прим.пер.: Практически тройная игра слов:
Time flies when you're having functionally – Время летит, пока вы “функциональничаете”
Time flies when you're having fun – Время летит, пока вы бездельничаете
Time flies when you're having functionally – Время летит, пока вы заняты делом
|
К сожалению, именно этот код вообще не является функциональным ...
или хотя бы практичным. Например, если бы я захотел изменить текст
выводимого сообщения, мне бы пришлось изменить тело метода oncePerSecond.
Рядовой Java-программист определил бы в oncePerSecond параметр с типом
String для передачи такого сообщения. Но даже такой подход крайне ограничен:
Любая другая периодическая задача (допустим, пингование удаленного сервера)
будет нуждаться в своей собственной версии oncePerSecond
— что является
прямым нарушением правила "Не повторяйся".
Как показано в листинге 4, замыкания предлагают гибкую и мощную альтернативу:
Листинг 4. Timer2.scala
object Timer
{
def oncePerSecond(callback: () => Unit): Unit =
{
while (true)
{
callback()
Thread.sleep(1000)
}
}
def timeFlies(): Unit =
{ Console.println("Time flies when you're having fun(ctionally)..."); }
def main(args: Array[String]): Unit =
{
oncePerSecond(timeFlies)
}
}
|
Теперь ситуация становится интереснее. В листинге 4 функция
oncePerSecond принимает параметр, но его тип выглядит странно.
Формально, в качестве значения параметра callback принимается функция.
Это справедливо до тех пор, пока передаваемая функция сама не имеет
входных параметров (обозначено как "()"), и возвращает
(обозначено "=>") «ничего» (функциональное значение "Unit").
Далее обратите внимание — в теле цикла я использую callback для
вызова переданного в параметре объекта-функции.
К счастью, где-то в программе у меня есть такая функция,
а именно timeFlies. Поэтому я просто передаю ее в
oncePerSecond при вызове из main.
(Вы также заметите, что в timeFlies задействован
Scala-специфичный класс Console,
служащий для той же цели, что и System.out или
же новый класс java.io.Console. Это чисто эстетический вопрос;
здесь будут работать и System.out и Console.)
Анонимная функция, какова же твоя функция?
Сейчас функция timeFlies выглядит как нечто бесполезное —
после всех усилий у нее, на самом деле, нет другого назначения, как только быть
переданной в oncePerSecond.
Раз так, я бы вообще не хотел формально ее определять, как показано в листинге 5:
Листинг 5. Timer3.scala
object Timer
{
def oncePerSecond(callback: () => Unit): Unit =
{
while (true)
{
callback()
Thread.sleep(1000)
}
}
def main(args: Array[String]): Unit =
{
oncePerSecond(() =>
Console.println("Time flies... oh, you get the idea."))
}
} |
В листинге 5 функция main передает произвольный блок кода в
качестве параметра oncePerSecond, это выглядит как лямбда-выражение
из Lisp или Scheme, что, само по себе, является другой разновидностью
замыкания. Такая анонимная функция опять демонстрирует мощь
отношения к функциям как к гражданам "первого сорта",
позволяя вам обобщать код таким совершенно новым способом, не прибегая к
механизму наследования. (Поклонники шаблона Strategy, вероятно,
уже начали неконтролируемо исходить слюной.)
Но на самом деле, oncePerSecond по-прежнему специфична:
она завязана на неразумное ограничение в том, что callback будет вызываться каждую секунду.
Я могу дальше продвинуться в обобщении, указав второй параметр, задающий —
как часто вызывать переданную функцию, что и показано в листинге 6:
Листинг 6. Timer4.scala
object Timer
{
def periodicCall(seconds: Int, callback: () => Unit): Unit =
{
while (true)
{
callback()
Thread.sleep(seconds * 1000)
}
}
def main(args: Array[String]): Unit =
{
periodicCall(1, () =>
Console.println("Time flies... oh, you get the idea."))
}
} |
Это распространенная практика в функциональных языках:
создать абстрактную функцию верхнего уровня, выполняющую некоторую работу,
передать в нее блок кода (анонимную функцию) как параметр, и вызвать
этот блок кода внутри высокоуровневой функции. Например, при переборе
коллекции объектов. Вместо использования в цикле традиционного для Java
объекта-итератора, функциональная библиотека предлагает взамен функцию —
обычно называемую "iter" или "map" — в коллекционных классах она принимает
функцию с одним параметром (объектом, подлежащим итерированию).
Таким образом, например, упомянутый ранее класс Array
содержит функцию filter,
определенную в листинге 7:
Листинг 7. Часть листинга Array.scala
class Array[A]
{
// ...
def filter (p : (A) => Boolean) : Array[A] = ... // не показано
}
|
Листинг 7 декларирует, что p
— функция, принимающая параметр обобщенного
типа A и возвращающая логическое значение.
Документация Scala утверждает, что filter "возвращает массив,
состоящий из всех элементов исходного массива, удовлетворяющих
предикату p." Это значит, что если я захочу на мгновение вернуться
к моей программе Hello World и найти все аргументы командной строки,
начинающиеся с буквы "G", это будет записано как в листинге 8:
Листинг 8. Привет, люди-G!
object HelloWorld
{
def main(args: Array[String]): Unit = {
args.filter( (arg:String) => arg.startsWith("G") )
.foreach( (arg:String) => Console.println("Найдено " + arg) )
}
}
Прим.пер.: G-man – агент ФБР
|
Здесь filter принимает предикат —
анонимную функцию, неявным образом возвращающую логическое значение boolean
(как результат вызова startsWith())
и вызывает этот предикат для каждого элемента в массиве "args".
Если предикат возвращает истина, filter
добавляет такой элемент в результирующий массив.
После перебора всего массива, результирующий массив возвращается и немедленно
используется в качестве исходного для вызова "foreach", который выполняет именно то,
что предполагает: foreach принимает другую функцию и
применяет ее к каждому элементу массива (в данном случае — просто отображает в консоль)
Не слишком сложно представить, как мог бы выглядеть Java-эквивалент рассмотренного
выше кода и не слишком трудно признать, что версия Scala гораздо, гораздо короче и
намного очевиднее.
Заключение
Программирование в Scala подкупающе просто и в то же время — необычно.
Его простота в том, что вы продолжаете работать с теми же базовыми объектами Java,
знакомыми и любимыми вами на протяжении многих лет, а очевидное отличие — в способе,
которым вам предлагается осмысливать нисходящую декомпозицию программы на части.
В этой первой статье из серии Путеводитель по Scala для Java-разработчиков
я лишь бегло ознакомил вас с возможностями Scala. То ли еще будет,
а пока — успешного "функционализирования"!
Ресурсы Научиться
-
Оригинал статьи:
The busy Java developer's guide to Scala: Functional programming for the object oriented (EN).
-
Scala Web
site: сайт, посвященный языку программирования Scala.
- "Java EE meets Web 2.0 (EN)"
(Constantine Plotnikov, Artem Papkov, Jim Smith; developerWorks, ноябрь 2007г.): статья раскрывает основные несоответствия между платформами Java EE и Web 2.0 и предлагает технологии (в т.ч. Scala), способные решить эти проблемы.
- "
Java theory and practice: Stick a fork in it" (EN) (Brian Goetz, developerWorks, ноябрь 2007г.): абстракция типа fork-join предлагает Java-механизм декомпозиции нескольких алгоритмов, обеспечивающий эффективное использование аппаратного параллелизма.
- "Functional programming in the Java language" (EN) (Abhijit Belapurkar, developerWorks, июль 2004г.): рассказывает о преимуществах и методах функционального программирования с точки зрения Java-разработчика.
-
Programming in Scala (EN)
(Martin Odersky, Lex Spoon, and Bill Venners; Artima, декабрь 2007г): первое
книжное издание - введение в Scala, в соавторстве с создателем Scala Мартином Одерски.
-
Технология Java: сотни статей по каждому аспекту Java-программирования.
Получить продукты и технологии
-
Scala: скачайте Scala и
начните обучение по материалам этой серии!
Обсудить
Об авторе  | |  | Тед Ньювард - глава Neward & Associates, где он консультирует, руководит,
обучает и внедряет Java, .NET, XML Services и другие платформы. Он проживает возле Сиэтла, штат Вашингтон. |
Выскажите мнение об этой странице
|  |