5/11/2014 5:26:29 AM
В прошлой заметке я рассказывал о трейтах (traits). Сегодняшняя заметка будет о неявных (implicit) значениях и параметрах.

Неявные значения и параметры играют очень существенную роль в Scala, поэтому можно говорить либо очень мало, либо очень много :) Я попробую держаться “золотой середины”.

Неявные параметры

  def visit(user: UserId)(implicit context: HttpContext) = {
    Visit(user, context.referrer)
  }

Функция “visit” определяет две группы параметров, одна из которых объявлена неявной. В Scala неявной можно объявить только одну, последнюю, группу параметров (количество параметров в группе может быть разным).
Функция с “неявными” параметрами ведёт себя точно так же, как и любая другая функция и может быть вызвана точно так же, как если бы ключевого слова implicit не было:
  val v = visit(UserId("alexey))(httpContext)
Существенное отличие состоит в том, что мы можем вызвать функцию “visit без указания неявной группы параметров! В этом случае компилятор попробует отыскать в известном ему скопе неявное значение нужного типа и самостоятельно подставить это значение в качестве параметра функции.
  scala> visit(UserId"alexey))
  <console>:error: could not find implicit value for parameter context: HttpContext
При вызове функции “visit” без указания неявных параметров возникла ошибка компиляции, говорящая о том, что компилятор не смог найти неявное значение типа HttpContext для того, чтобы передать его в качестве неявного параметра.

Мы можем удовлетворить компилятор указав неявное значение в доступном скопе:
  implicit val currentContext = HttpContext.current

  val v = visit(UserId("alexey"))
В данном случае вызов функции “WebTracker.visit” будет скомпилирован успешно поскольку компилятор сумеет найти неявное значение, удовлетворяющее параметру функции.

Я несколько перепишу и расширю этот пример чтобы проиллюстрировать ещё и вариант применения и материал предыдущих заметок :)
trait ProvidesHttpContext {
  implicit val context = HttpContext.current
}

object WebTracker {
  import HttpContext._

  def visit(user: UserId)(implicit context: HttpContext) = {
    Visit(user, context.referrer)
  }
}

class ImplicitContextExample extends ProvidesHttpContext {
  val v = WebTracker.visit(UserId("alexey"))
}
Давайте подробно рассмотрим, что только что произошло.
Трейт ProvidesHttpContext содержит только одно значение текущего контекста, причём это значение объявлено неявным.
Модуль WebTracker определяет уже знакомый метод “visit” с неявным параметром context.
Класс ImplicitContextExample является примером использования модуля WebTracker. Этот модуль смешивает трейт ProvidesHttpContext так, что неявное значение контекста оказывается в скопе, доступном при вызове функции “visit” модуля WebTracker.

Таким обр��зом мы разделили ответственность модулей:
- Модуль ProvidesHttpContext просто предоставляет контекст не зная ничего о том, как он будет использоваться.
- Внутри класса ImplicitContextExample можно не беспокоиться о явной передаче контекста вызывая метод “visit” (и, возможно, другие методы, зависящие от контекста). Эту работу берёт на себя компилятор. Код выглядит проще и красивее. Обычно программисты добиваются такого эффекта путём объявления поля/свойства класса и использования значения напрямую из функции чтобы не “таскать” значение через все параметры функций, однако я бы назвал такой метод “грязным” и всячески не рекомендовал бы пользоваться такими способами.
- Модуль WebTracker не знает о том, откуда берётся контекст. Метод “visit” представляет собой “настоящую” функцию, зависящую только от своих параметров и не использующую никакого внешнего окружения, что всегда очень хорошо :)
- Мы можем использовать (и тестировать) функцию “visit” явно передавая нужный (тестовый) контекст, в этом случае компилятор не будет вмешиваться.
- Мы могли бы не объявлять трейт ProvidesHttpContext вообще, а просто объявить значение context прямо внутри класса-примера, но я хотел показать и ещё одну вещь. В данном случае, как и в одной из прошлых заметок, мы можем иметь несколько трейтов, предоставляющих разные виды контекста. Например, мы можем сделать FakeContext для тестирования и смешивать его с ProvidesHttpContext там, где это необходимо. Таким образом, благодаря линеаризации, просто смешивая трейт мы можем влиять на то, какой именно контекст будет использован функциональностью класса.

Довольно удобно использовать неявные параметры и значения с параметризованными типами:
trait Formatter[-A] { def format(a: A) : String }

object Formatters {
  implicit object LongFormatter extends Formatter[Long] {
    def format(a: Long) = s"(Long: $a)"
  }

  implicit object AnyListFormatter extends Formatter[List[_]] {
    def format(a: List[_]) = s"Any list: [${a.mkString(" :: ")}]"
  }
}

object Printer {
  private val defaultFormatter = new Formatter[Any] {
    def format(a: Any) = a.toString
  }

  def print[A](a: A)(implicit logger: Formatter[A] = defaultFormatter) = {
    println(logger.format(a))
  }
}

object PrinterExample extends App {
  import Formatters._

  Printer.print(42)          // 42
  Printer.print(12L)         // (Long: 12)
  Printer.print(List(1,2,3)) // Any list: [1 :: 2 :: 3]
}
Трейт Formatter представляет собой интерфейс для форматирования значения в строку (метод “format”). Значок “минус” у типа-параметра в Scala означает контравариантность, аналог ключевого слова “in” в C#, аспекты вариантности нас сейчас не интересуют.

Модуль Formatters содержит несколько специфицированных реализаций для форматирования значений. Так мы имеем форматтер для значений типа Long и ещё один для списков любого типа. Запись List[_] называется “existential type” и означает “это должен быть параметризованный (generic) лист, но конкретный тип при параметризации может быть любым и нас сейчас не интересует”. Фактически это означает, что функция будет способна работать с листом любого типа. В C# и F# возможность existential types отсутствует.

Модуль Printer предоставляет функциональность для вывода значений на экран. Внутри он содержит форматтер “по умолчанию”, который просто возвращает значение toString для экземпляра любого типа.
Метод “print” принимает значение для печати а так же неявное значение форматтера, по умолчанию установленное в defaultFormatter так, что компилятор не будет ругаться в случае, если не сумеет найти подходящего значения. Метод “print" печатает на экран отформатированное с помощью форматтера значение переданного экземпляра.

Модуль PrinterExample показывает пример использования.
Для начала производится импортирование содержимого модуля Formatters. Директива import в Scala - аналог директивы using в C# с тем исключением, что может быть использована везде: в начале файла, в классе, в методе. Кроме того import позволяет импортировать не только неймспейсы, но и конкретные типы и модули и даже конкретные методы. В данном случае производится импорт всего содержимого модуля. 

Вызовы метода “print” просто печатают на экране отформатированные значения. То, какие именно форматтеры будут использованы в каком случае определяет уже компилятор! Как он это делает, я думаю, понятно: для любого переданного в параметр значения типа T будет осуществлён поиск неявного значения типа Formatter[T]. Если таковое будет обнаружено в скопе поиска, то данный форматтер использован в качестве параметра функции, если же нет, то будет использовано значение "по умолчанию”.
Всегда можно написать новых форматтеров, импортировать ещё несколько модулей с форматтерами и т.д. без необходимости менять имеющийся код.

Неявное преобразование типа


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

В качестве самого простого примера неявного преобразования мы можем написать аналог extension methods, имеющихся в C#.
object Extensions {
  implicit class IntExtensions(i: Int) {
    def percentOf(hundred: Int) = i * 100 / hundred
  }
}
Модуль Extensions определяет неявный класс IntExtensions, принимающий Int в качестве параметра конструктора. Метод “percentOf” этого класса вычисляет значение доли в процентах от значения hundred, которое принимается за 100%.
Теперь, импортировав класс IntExtensions из модуля Extensions, метод “percentOf” можно использовать как если бы это был метод класса Int:
scala> import Extensions._
scala> 12.percentOf(100)
res2: Int = 12

scala> 12 percentOf 50
res3: Int = 24
В обоих случаях метод “percentOf” вызывается для значения 12, которое имеет тип Int (во втором случае используется синтаксис Scala, о котором я говорил ранее).
В процессе компиляции компилятор “видит”, что тип Int не определяет метода “percentOf”. В этом случае компилятор пытается найти в доступном скопе поиска неявный тип, способный удовлетворить следующему условию: на вход конструктора принимается значение типа Int и содержащего метод “percentOf(_ : Int)”. Если компилятор сумел найти подходящий класс, то он будет использован для конвертации, еcли же нет, то произойдёт ошибка компиляции.
В данном случае ошибки компиляции не происходит потому, что в начале я импортировал содержимое модуля Extensions; теперь компилятор способен найти неявный класс IntExtensions.
 
Теперь можно по-новому посмотреть на следующие конструкции Scala:
(1 to 5) foreach println
(1 until 5) foreach println
В результате выполнения будут напечатаны списки значений от 1 до 5 и от 1 до 4.

Конструкции (1 to 5) и (1 until 5) не являются синтаксисом языка Scala. Здесь используется примерно тот же трюк, что и в примере с процентами: Int не определяет методов “to” и “until”, поэтому компилятор выполняет поиск для неявного преобразования.

Заключение


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

Этой заметкой я завершаю цикл “Scala для C# разработчиков” и планирую посвятить несколько следующих заметок базовым концепциям и аспектам функционального программирования. Я буду использовать Scala для контекста, надеюсь, этот цикл заметок поможет пониманию примеров.


5/5/2014 2:25:32 PM
В прошлой заметке я говорил о функциях, их синтаксисе и некоторых особенностях в Scala.
Сегодня мы поговорим о трейтах (traits).

Определение трейтов


В прошлых частях серии я несколько раз употреблял фразу о том, что C#-аналогами трейтов (trait) в Scala в первом приближении могут выступать интерфейсы (interface).
Действительно, трейты могут выполнять функции интерфейсов, однако их возможности гораздо шире.

Одним из главных отличиев трейта от интерфейса является то, что трейты могут (хоть и не обязаны) содержать реализацию:
trait Logger {
  def log(text: String)
}

trait ConsoleLogger extends Logger {
  override def log(text: String) = println(text)
}
Трейт Logger определяет один нереализованный (абстрактный) метод “log”.
Трейт ConsoleLogger расширяет возможности трейта Logger, определяя метод “log” для печати сообщения в консоль. Заметьте, что при этом в трейте ConsoleLogger не остаётся нереализованных (абстрактных) методов, но от этого он не перестаёт оставаться трейтом. Мы могли бы объявить ConsoleLogger как class, но при этом мы потеряли бы ряд интересных возможностей, которые рассмотрим ниже.

Использование трейта синтаксически мало чем отличается от использования интерфейсов в C#, однако существует семантическая разница и разница в терминологии. В терминологии C# мы говорим “класс реализует интерфейс”, в терминологии Scala мы говорим используем термин “смешан” (mixed in) и говорим “трейт смешан с классом”.
Согласен, что по-русски это звучит довольно странно, но как иначе? "Миксует”? Ещё хуже...).
Для такой терминологии есть свои причины, которые мы рассмотрим чуть ниже, а пока давайте будем ею пользоваться.
class Worker extends ConsoleLogger { }
class Engine extends BaseEngine with ConsoleLogger { }
Класс Worker использует трейт ConsoleLogger.
Класс Engine наследует базовый класс BaseEngine и смешивает (Примешивает? хм… Мне этот термин кажется несколько смешным, но я продолжу его использовать) трейт ConsoleLogger.

В Scala отсутствует множественное наследование классов (класс может наследовать только один базовый класс, абстрактный либо нет), но может “смешивать” множество трейтов.
Поэтому в примере выше мы определили ConsoleLogger как трейт, а не как абстрактный класс: это помимо прочего позволяет не вмешиваться в иерархию наследования, а как-бы добавлять, “смешиваться” с нею.

Вообще говоря, считается, что иерархия наследования отвечает на вопрос “рождён как” (born as), в то время, как трейт отвечает на вопрос “способен предоставить функциональность” (capable of). Например, наш класс Engine “рождён как” BaseEngine и при этом “способен предоставить функциональность” логирования. То же самое мы можем сказать и об интерфейсах, с той лишь разницей, что трейты могут самостоятельно (полностью или частично) отвечать за предоставляемую функциональность.

Например, нередко можно встретить объявления классов вида:
class MyHandler extends Handler with Configuration with Statistics with Logging { 
  ??? 
}
Класс MyHandler является потомком базового типа Handler и при этом смешивает трейты Configuration, Statistics и Logging для того, чтобы получить доступ к соответствующей функциональности, реализованной в этих трейтах.


Self-types и ограничения на смешивание


Ещё одним отличием трейтов Scala от интерфейсов C# является возможность накладывать ограничение на то, какой тип будет иметь право смешивать этот трейт:
trait Warnings { self: Logger =>
  def warn(text: String) = log(s"[WARN] $text")
}

trait Errors {self: Logger =>
  def error(text: String) = log(s"[ERROR] $text")
}
Трейты Warnings и Errors определены так, что для возможности их смешивания необходимо, чтобы тип, в который происходит смешивание, уже имел в своём составе Logger. Иными словами мы декларируем зависимость: для того, чтобы использовать Warnings или Errors нам необходимо, чтобы был доступен Logger, поскольку эти трейты его используют.
Имя значения self может быть любым. Наиболее часто используются идентификаторы self, this, me и т.д.

Теперь, если мы хотим воспользоваться возможностью записи в лог предупреждений и ошибок, мы можем определить класс Worker как:
class Worker extends ConsoleLogger with Errors with Warnings { }
Заметьте, что если бы мы не указали ConsoleLogger в этом списке, то компилятор выдал бы ошибку, заявив, что для смешивания трейтов Errors и Warnings необходимо, чтобы Worker смешивал какой-нибудь Logger.

Такая композиция позволяет более чётко разделить обязанности (capable of, каждый трейт отвечает за свой кусочек функциональности) и сохранить гибкость: мы можем использовать не только ConsoleLogger, но и любой Logger без необходимости что-то менять в функциональности трейтов Errors и Warnings. Например, мы могли бы реализовать FileLogger, или даже просто смешать “базовый” Logger определив функцию “log” внутри класса; трейты Errors и Warnings будут корректно работать.

Анонимные типы


Говоря о создании экземпляров нужно упомянуть возможность Scala создавать экземпляры “анонимных” типов:
  val log = new Object with ConsoleLogger with Warnings
  log.warn("Anonymous object detected! Can you do it in C#?")
log имеет значение анонимного типа, сконструированного как экземпляр типа Object с трейтом ConsoleLogger. Понятно, что вместо Object можно использовать другие типы. Такой синтаксис позволяет создавать экземпляры и “смешивать” ноль или более трейтов без необходимости декларации отдельного типа. В случае если вдруг понадобился экземпляр “такой же, но с перламутровыми пуговицами” это очень удобно. В фигурных скобках можно расширять функциональность экземпляра: переопределять (override) методы, добавлять новые методы и т.д. Фактически данный синтаксис декларирует новый тип и тут же создаёт его экземпляр.
Такой приём довольно часто используется таких случаях, как, например, возврат Closable/Disposable значений:
  def subscribe(id: String) = {
    new Closable {
      def close() = println(s"Subscriber $id has been unsubscribed")
    }
  }
Допустим, функция “subscribe” создаёт некую подписку (как и что она делает нам сейчас не важно). В качестве своего результата “subscribe” возвращает экземпляр “анонимного” типа в котором смешан трейт Closable.
Запись "new <trait>” является синонимом записи new “new Object extends <trait>”: поскольку класс экземпляра не указан, то используется Object.

Множественное наследование, Diamond Problem и линеаризация.


Поскольку трейты могут содержать не только объявление интерфейса, но и функциональность, может возникнуть вопрос: а в чём отличие трейтов от абстрактных классов?

Одним из важных отличий является то, что трейты не могут определять конструкторов с параметрами. Действительно, трейты “смешиваются” с конструируемым экземпляром и (на сегодняшний день), как и интерфейсы C#, не могут диктовать синтаксис конструкторов, в то время, как абстрактный класс может иметь конструкторы с параметрами.
Вторым важным отличием является то, что класс может наследовать только один класс (абстрактный либо нет), но может смешивать множество трейтов.

Таким образом мы можем заключить, что при смешивании нескольких трейтов в Scala происходит множественное наследование.
Одной из основных проблем множественного наследования является т.н. “Ромбовидное наследование” (diamond problem): допустим, некий класс Liger является потомком типов Lion и Tiger, которые в свою очередь являются потомком Mammal, таким образом мы имеем “ромбовидную” иерархию.
Базовый тип Mammal определяет метод “sound”, отвечающий за издавание устрашающего звука животным. Типы Lion и Tiger переопределяют этот метод. Проблема состоит в том, какой из методов “sound” в этом случае должен быть использован типом Liger?

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

Простыми словами и в первом приближении мы можем описать следствия линеаризации как “последний побеждает”. Это даёт гибкие возможности расширения и переопределения функциональности классов, например:
  trait NoLogger extends Logger {
    override def log(text: String) = {}
  }

  class Worker extends ConsoleLogger with Errors with Warnings { }
  
  val silentWorker = new Worker with NoLogger
  silentWorker.warn("Silence will fall when the question is asked")
Трейт NoLogger определён как потомок трейта Logger так, что он игнорирует любые попытки записи в лог.
Класс Worker определён так же, как и ранее: он использует ConsoleLogger для вывода лога в консоль, а так же трейты Errors и Warnings для категоризации сообщений лога.
Идентификатор silentWorker принимает значение типа Worker with NoLogger. Как мы видим, семантика объявления типа совпадает снамерениями (и с результатом): мы “просто” создаём экземпляр типа Worker который, в отличие от поведения по умолчанию, будет игнорировать попытки записи в лог.

Естественно, silentWorker не обязан быть экземпляром анонимного типа, с тем же успехом мы могли бы явно объявить тип и затем создать его экземпляр, как вынуждены были бы делать это в C#. Здесь и далее я буду использовать анонимные типы для краткости и простоты.

Заметьте, что даже в случае такого простого и наивного примера мы уже получаем определённую гибкость и модульность. Например, мы могли бы определить класс Worker использующим NoLogger по умолчанию и смешивать ConsoleLogger только тогда, когда нам необходимо писать в лог.
Или мы могли бы определить трейт IgnoreWarnings и смешать его так, чтобы полученный с его смешением экземпляр игнорировал предупреждения, но “пропускал” бы сообщения об ошибках.

Помимо прочих вариантов такая техника очень удобна и в юнит-тестировании: мы можем тестировать класс “переопределив” один или несколько его аспектов, например, отключив лог, или предоставив тестовую конфигурацию, или примешав фейковое соединение с базой данных.

Наращиваемая (stackable) функциональность с помощью трейтов и линеаризации


Трейты могут не только замещать, но и расширять функциональность друг друга.
trait Animal { def sound : String }
trait Mammal extends Animal { def sound = "" }

trait Lion extends Mammal {
  override def sound = s"Rwooorrw! ${super.sound}"
}
trait Tiger extends Mammal {
  override def sound = s"Meow... ${super.sound}"
}
Трейт Animal определяет абстрактный метод “sound”.
Трейт Mammal представляет собою класс млекопитающих и определяет поведение по умолчанию для метода “sound”: животное молчит.
Трейты Lion и Tiger являются потомками трейта Mammal и определяют собственные реализации метода “sound”. При этом оба этих типа вызывают метод “sound” “базового” типа так, чтобы после издания собственного воинственного крика учесть специфику базового типа животного.

Давайте подумаем, что будет являться результатом в случае, когда некий тип смешивает оба этих трейта?
  val lionWithTiger = new Lion with Tiger
  val tigerWithLion = new Object with Tiger with Lion
Идентификаторы lionWithTiger и tigerWithLion смешивают оба трейта Lion и Tiger, но в разном порядке.
Во втором случае я явно использовал new Object просто чтобы показать возможность использования какого-либо типа для смешивания трейтов. Первый случай, без использования Object, аналогичен второму: компилятор сам добавит Object если мы не указали тип в который будут смешиваться трейты.

Итак, что будет, например, если вызвать метод “lionWithTiger.sound”? Какой из методов будет использован? И что случится когда один из этих методов вызовет метод базового класса “super.sound”?

Ответ на первый вопрос мы уже знаем: “победит последний”, то есть, при вызове “lionWithTiger.sound” будет вызван метод трейта Tiger.

Ответ на второй вопрос тоже кроется в линеаризации, но давайте сначала посмотрим на результат:
  scala> lionWithTiger.sound
  res1: String = " Rwooorrw! Meow..."

  scala> tigerWithLion.sound
  res2: String = " Meow... Rwooorrw!"

В процессе линеаризации иерархии компилятор выстраивает линейную иерархию таким образом, что “super” указывает на “предыдущий” трейт!

С помощью такой возможности Scala мы можем наращивать функциональность и создавать нужную композицию путём изменения порядка смешивания трейтов.

Напоследок - небольшой пример такой композиции.
Удачливый охотник всегда сыт! Определим аспект удачливого охотника в отдельный трейт:
  trait LuckyHunter extends Mammal {
    override def sound = s"Nom nom... ${super.sound} nom nom nom"
  }
Трейт LuckyHunter представляет собою млекопитающее, которое в данный счастливо момент ест (произносит ням-ням-ням).

Теперь мы легко можем сделать любое млекопитающее сытым примешав к нему трейт LuckyHunter:
  scala> val luckyTiger = new Tiger with CatchedPray
  scala> luckyTiger.sound

  scala> res3: String = Nom nom...  Meow... nom nom nom
Чем не аспектно-ориентированное программирование? :)

Заключение


Трейты в Scala играют важную роль выступая не только в качестве интерфейсов, но и открывая новые (по сравнению с C#) возможности композиции. Хотя в Scala класс может наследовать только один базовый класс, он так же может наследовать несколько трейтов. Линеаризация иерархии в Scala позволяет элегантно решить проблему “ромбовидного наследования”, позволяя этому решению выглядеть интуитивно понятно с точки зрения семантики.

Можно ли говорить о том, что Scala поддерживает множественное наследование?
Ответ на этот вопрос вы можете дать для себя сами. Одни программисты признают такое поведение де-факто множественным наследованием, считая несущественным “детали реализации”. Другие программисты не считают это множественным наследованием по причине наличия правила наследования одного класса и линеаризации.
А каково ваше мнение?

В следующей заметке мы поговорим о “неявных” (implicit) параметрах и связанных с этой интересной концепцией аспектах, после чего перейдём непосредственно к функциональному программированию на Scala.
 

Tags:

5/2/2014 2:31:46 PM
В прошлый раз мы рассмотрели кейс-классы (case classes) и сопоставление с образцом (pattern matching) в Scala, немного углубившись в “механику” их работы.
Сегодня мы поговорим о таком базовом элементе языка, как функции.

Синтаксис функций


Как мы уже видели, для определения функций-членов класса или объекта используется ключевое слово “def” (от definition).
Согласно конвенции имена функций, свойств, полей и параметров в Scala (и многих других функциональных языках) начинаются с маленькой буквы, а имена типов - с большой.
  def add(x: Int, y: Int) = x + y
Функция “add” принимает два параметра типа Int и возвращает результат их суммы.

В отличие от C# в Scala не нужно использовать ключевое слово “return” для возврата значения из функции. Вместо этого функция возвращает значение последнего вычисленного выражения. Можно смотреть на это таким образом: функция что-то делает, делает, делает… Вот что осталось в результате в конце - то и есть результат её работы :)

В Scala в большинстве случаев не обязательно указывать тип возвращаемого функцией значения: в основном компилятор способен вывести этот тип в процессе разбора тела функции. В некоторых случаях, однако, требуется указывать тип возвращаемого значения. Такими случаями являются, например, рекурсивные функции и перегруженные методы (overload).
  def sum(acc: Int, values: List[Int]) : Int = 
    values match {
      case List() => acc
      case head :: tail => sum(acc + head, tail)
    }
sum” - пример рекурсивной функции, тип возвращаемого значения которой задан явно. Функция принимает “начальное” значение аккумулятора и список значений, которые нужно суммировать. В случае, если список пустой “sum” просто возвращает значение аккумулятора. В случае, если список не пуст, значение первого элемента списка (head) суммируется со значением аккумулятора и “sum” рекурсивно вызывается с новым значением аккумулятора для оставшегося “хвоста” (tail) списка.

В отличие от C# компилятор Scala поддерживает “хвостовую оптимизацию” для рекурсивных функций (tail call optimisation), разговор о которой не входит сейчас в наши цели.

Функции в Scala могут быть “вложенными”, то есть, мы можем определять функции внутри функций. Например, мы можем переписать функцию “sum” таким образом, чтобы скрыть рекурсию и использование аккумулятора:
  def sum(values: List[Int]) = {
    def loop(acc: Int, values: List[Int]): Int =
      values match {
        case List() => acc
        case head :: tail => loop(acc + head, tail)
      }
    
    loop(0, values)
  }
Рекурсивная функция в этом случае объявлена “внутри” функции “sum” и недоступна извне. Результирующим значением “sum” является значение, вычисленное функцией “loop(0, values)” так, как именно это действие является последним выражением, выполняемым функцией.
Вложенные функции удобны в качестве вспомогательных элементов, которые имеют смысл только внутри данной функции и которые нет смысла выносить в качестве (пусть и приватных) методов класса.

Типы функций


В Scala тип функции может быть описан выражением T => K. Это означает, что в процессе своей работы функция трансформирует значение некого типа T в значение некого типа K, который является возвращаемым значением функции. Такая нотация является обычной для функциональных языков и читается как “функция из T в K”.
Например, функция “add” имеет тип (Int, Int) => Int (“из пары Int, Int в Int") , что означает, что функция “add” принимает два Int и возвращает результат типа Int.
Функция “sum” имеет тип List[Int] => Int, а функция “loop” имеет тип (Int, List[Int]) => Int.

В случае с C# Можно провести аналогию с типом Func<int, int, int>, хотя последний, на мой взгляд, читается сложнее, чем (Int, Int) => Int.

Функции высших порядков


Функциями высших порядков называются функции, принимающие другие функции в качестве параметров или возвращающие функции в качестве результата.
Например наша функция “sum” является частным случаем агрегации списка значений. Мы можем обобщить агрегацию списка в виде:
  def fold(init: Int, values: List[Int], f: (Int, Int) => Int) : Int = 
    values match {
      case List() => init
      case h :: tail => fold(f(init, h), tail, f)
    }
  
  val list = List(1,2,3,4,5)
  
  val sum = fold(0, list, (a, b) => a + b)
  val product = fold(1, list, _ * _)
Функция “fold” принимает в качестве параметров начальное значение, список и функцию, с помощью которой будет производиться агрегация значений. Попробуйте записать тип функции “fold” в качестве упражнения :)

Ближайшим аналогом "fold" в C# является метод Aggregate(…) из LINQ. В LINQ Microsoft немного “изобрели” собственную терминологию, переназвав операции с “общепринятыми” именами, такие, как fold, map, filter и т.д. как Aggregate, Select, Where и т.д.

С помощью “fold” мы можем подсчитать сумму и произведение списка, а так же, при желании, агрегировать список любым понравившимся способом. 

Получение значения суммы элементов выглядит так же, как и в C#, а вот получение произведения записано в несколько ином виде. Синтаксис “_ * _” является “укороченным” вариантом лямбда-функции в Scala, мы как-бы говорим, что хотим передать операцию “умножить” и нам не важно давать имена параметрам. В этой нотации первый знак подчёркивания означает первый параметр, второй - второй параметр и т.д.

Например, трансформацию списка с помощью “map” (в C# аналогом будет функция Select) мы можем записать в двух видах:
  list.map(x => x.toString)
  list.map(_.toString)
Ну и ещё одна особенность Scala: круглые скобки можно заменить на фигурные, а точки можно заменять пробелами. Так, вызов map может быть записан в видах:
  list.map {x => x.toString }
  list map { _.toString }
Конечно, фигурные скобки в данном случае представляют собой блок, а блок в Scala - это выражение и, как любое выражение, возвращает значение. То есть, по сути блок - это функция, так что никакой подмены понятий тут не происходит.

Возможно это поначалу покажется странным, но посмотрите как элегантно может выглядеть, например, юнит-тест если опустить ненужные с точки зрения читабельности точки (и это на самом деле синтаксис ScalaTest, а не придуманные мною конструкции):
  dict("id") should be (expectedId)
  evaluating { divide(2, 0) } should produce [ArithmeticException]
  usersList should have size 5

Группы параметров


В Scala мы можем создавать функции, принимающие не одну, а несколько групп параметров. Вот несколько исправленный и несколько обобщённый пример нашей функции для агрегации листа:
  def fold[T](init: T)(values: List[T], f: (T, T) => T): T = values match {
    case List() => init
    case h :: tail => fold(f(init, h))(tail, f)
  }

  fold(1)(List.range(1, 20), (a, x) => a * x)
  fold(0)(List(1,2,3,2,1), _ ^ _)
Функция “fold”, способная теперь агрегировать не только лист значений любого типа, принимает две группы параметров: начальное значение в первой группе и пару список плюс функцию-аккумулятор во второй. Вызов такой функции осуществляется таким же образом: различные группы параметров передаются в скобках.

Первый вариант использования представляет вычисляет произведение списка.
Второй вариант использует “короткий” способ передачи лябмда-функций и решает популярную задачку “есть у нас список в котором все значения дублируются, кроме одного, нужно его найти”.

Группы параметров удобны в нескольких случаях.
Например, в некоторых случаях это может помочь в выводе типов. Не вдаваясь в подробности вывода типов, скажу, что анализ типов в Scala производится компилятором “слева направо”, причём группы параметров анализируются “целиком”. Таким образом, при использовании (вызове) функции “fold” компилятор сначала определит тип переданного параметра init и уже потом, при анализе второй группы параметров, содержащих лист и функцию, будет “знать” чему в случае конкретного вызова равен тип T и, соответственно, какой тип ожидается для листа и функции.
Попробуйте записать функцию “fold” так, чтобы она принимала только одну группу параметров и вы увидите, что компилятор потребует у вас явно указать типы параметров функций умножения и сложения.

В более общем виде функция "fold" могла бы выглядеть как
fold[T, K](init: K)(values: List[T], f: (K, T) => K) : K
или, что удобнее c точки зрения вывода типов и частичного применения (см. ниже), как:
fold[T, K](init: K)(values: List[T])(f: (K, T) => K) : K

Другой пример проще продемонстрировать наглядно:
  def retry[T](times: Int)(body: => T) : T = {
try { body } catch { case _ if times > 0 => retry(times-1)(body) } } val result = retry(5) { // do something.... }
Функция “retry” принимает две группы параметров. В первой группе находится параметр times (максимальное количество повторов), а во второй - функция body, не принимающая параметров и возвращающая результат типа T. Суть функции “retry” сводится к тому, что не принимающая параметров функция body будет выполняться снова и снова до тех пор, пока не будет выполнена успешно (и тогда “retry” вернёт вычисленное body значение), либо пока количество повторов не превысит допустимое (и тогда исключение “выплывет" наружу).
Использование “retry” выглядит красиво органично, как если бы retry было ключевым словом синтаксиса Scala.

Ещё один пример синтаксиса Scala для использования функций высшего уровня напрямую не связан с группами параметров, но отчасти использование групп параметров делают возможным такую запись (прежде, чем читать ответ, попробуйте догадаться почему):
  fold(0)(List.range(1, 20), {
    case (a, x) if x % 2 == 0 => a + x
    case (a, x) => a - x
  })
Этот пример использования “fold” вычисляет разность сумм чётных и нечётных чисел в списке целых чисел от 1 до 20.
Такой синтаксис очень удобен, поскольку, опять же, позволяет декларативно описать намерения.
Ответ на вопрос о том, почему группы параметров делают возможной такую запись кроется в выводе типов. Если бы “fold” имел всего одну группу параметров, компилятор не смог бы самостоятельно определить тип передаваемой функции (и, соответственно, тип значений a и x и нам пришлось бы указывать тип этих значений “вручную”. Невелика беда, но так красивее :)

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

Каррирование (Currying)


Давайте вернёмся к типам и рассмотрим тип функции, использующей две группы параметров:
  def add(x: Int)(y: Int) = x + y
Функция “add” теперь принимает не одну, а две группы параметров. Очевидно, что тип этой функции не может быть (Int, Int) => Int, как это было бы в случае одной группы параметров. В данном случае “add” имеет тип Int => Int => Int.
Что будет, если мы передадим функции только один параметр типа Int, а не два? Если следовать типу функции, то получается, что возвращаемым значением в этом случае будет тип Int => Int !
То есть, в случае передачи только одного параметра функции такого типа “не останется ничего другого”, как вернуть функцию, принимающую недостающий параметр и возвращающую результат.

Такое поведение функций называется “currying” (Каррирование, термин назван так по имени Хаскеля Карри, который ввёл эту технику в широкий обиход. Придумал же эту технику Шёнфинкел, так что более честно она могла была бы называться “шёнфинкгелинг”)
Функция называется “каррированной” если она ведёт себя таким образом, как наш пример с Int => Int => Int: принимает параметр, возвращает функцию, принимающую следующий параметр и т.д. пока дойдёт до результата.

Обратите внимание насколько интуитивна запись Int => Int => Int.
Теперь попробуйте записать этот тип на C#: Func<int, Func<int, int>>. Попробуйте рассмотреть более “сложные” примеры: Int => (Int, Int) => Int и преимущества этой нотации станут понятны.

В отличие от других функциональных языков (F#, Haskell, OCaml), в которых функции каррированы по умолчанию, в Scala это не так. Мы можем сразу записать функцию в “каррированном” виде, как мы сделали с функцией “add”, либо воспользоваться возможностями каррирования, описанными ниже.

Я думаю, что в общем случае мы можем рассматривать любую функцию как каррированную. В этом случае, например, на функцию (Int, Int) => Int можно смотреть не как на функцию, принимающую два параметра и возвращающую результат, а как на функцию, принимающую один параметр типа пары (tuple) (Int, Int) и возвращающую результат. Мне кажется, что при таком взгляде на вещи картина становится логичнее.

Так же мы могли бы записать функцию “add” в виде:
  def add(x: Int) = (y: Int) => x + y
Эта запись явно означает “принять параметр x и вернуть анонимную функцию, которая примет параметр y и вернёт сумму x и y”. Технически первый вариант с группами параметров является просто более короткой записью варианта с анонимной функцией.

Ещё одним вариантом записи могло бы быть:
  val add: Int => Int => Int = 
    x => y => x + y
Здесь “add” не метод класса, а значение типа Int => Int => Int, которое равно некой анонимной функции данного типа.

Хоть это и нетипично для C#, мы могли бы сделать там примерно то же самое, однако необходимость указывать тип переменной в виде нагромождения Func<…>, мне кажется, является одной из причин того, почему это практически не делается: просто неудобно. Второй причиной может являться отсутствие поддержки неизменяемости (immutability) в языке.

В Scala существует возможность каррировать любую имеющуюся функцию. Для этого можно использовать метод “curried”:
  def add(x: Int, y: Int) = x + y    // (Int, Int) => Int
  val sub: (Int, Int) => Int = _ + _ // (Int, Int) => Int

  val curriedAdd = (add _).curried   // Int => Int => Int
  val curriedSub = sub.curried       // Int => Int => Int
  
  val uncurriedAdd = Function.uncurried(curriedAdd) // (Int, Int) => Int
  val uncurriedSub = Function.uncurried(curriedSub) // (Int, Int) => Int
К сожалению, как мы видим, в Scala существует разница между методом (def) и значением (val), даже если оба представляют собою идентичные функции. С этим мелким различием мы будем иногда сталкиваться программируя на Scala.

Частичное применение (partial application)


Частичным применением называют применение функции посредством передачи не всех, а только части параметров. Выше я уже приводил пример с функцией типа Int => Int => Int, которой передан только один параметр. Поскольку, как следует из типа, функция каррирована, то в этом случае она просто вернёт функцию Int => Int в качестве результата. Это и называется “частичным применением”.

Рассмотрим частичное применение на примере:
  // String => String => Option[Element]
  def findElement(parent: String)(selector: String) : Option[Element] = ???

  def findInBody = findElement("body) _ // String => Option[Element]
  val findInContent = findElement("content") _ // String => Option[Element]
Допустим, что функция “findElement” (тело которой я не привожу, так как это сейчас не важно) способно по указанному селектору найти HTML Element внутри какого-то “родительского” элемента. В случае успешного находждения функция возвращает Some(element), в противном случае - None.

Функции “findInBody" и “findInContext” предоставляют более удобный и понятный интерфейс, нежели более общая функция “findElement” путём “фиксации” первого параметра (родительского элемента).
Для примера я объявил одну из функций как def, а вторую как val: частичное применение работает в обоих случаях.
Обратите внимание на знак подчёркивания в конце объявления с частичным применением. К сожалению, он в этом случае обязателен. В данном случае он означает “оставшийся хвост параметров” (которых в общем случае может быть несколько).
Согласен, в этом аспекте синтаксис Scala особой красотой не блещет, но и совсем уж ужасно тоже не выглядит :)

C другой стороны, если указать ожидаемый тип вручную, использования “подчёркиваний” можно избежать, хотя лично я предпочитаю как можно меньше явно указывать типы в коде, но выбор остаётся за вами:
  def findInBody: String => Option[Element] = findElement("body")
  val findInContent: String => Option[Element] = findElement("content")
Очень часто частичное применение используется при передаче функций в качестве параметров:
  def highlight(finder: String => Option[Element]) = ???

  highlight(findElement("section.article"))
Допустим, функция “highlight” как-то выделяет текст, но для работы ей необходимо передать функцию, способную осуществлять поиск элемента по селектору. Используя “highlight” мы передаём в качестве параметра функцию “findElement” с “фиксированным” первым параметром. Функция “findElement” здесь частично применена так, что создаёт некий контекст: всё, что будет делать функция “highlight” теперь не будет выходить за пределы “родительского” элемента “section.article”.
В данном случае частичного применения никакого знака подчёркивания не требуется поскольку компилятор уже заранее (из сигнатуры принимающей функции) “знает” тип ожидаемой параметром функции и способен вывести нужный результат самостоятельно.

Другой пример:
  def add(a: Int)(b: Int) = a + b
  List(1,2,3,4) map add(1) // List(2,3,4,5)
Мы передаём в качестве такой функции частично применённую функцию “add”, где один из параметров “фиксирован”. Частично применённая функция “add(1)” - это функция, которая принимает один параметр и возвращает функцию, принимающую один оставшийся параметр. Такая функция удовлетворяет сигнатуре “List[K]map: (T=>K) => List[K]” (map, как и Select, принимает функцию, трансформирующую значение в какое-то новое значение и применяет её ко всем элементам списка поочерёдно и возвращает новый лист).

Но что если исходная функция не каррирована? В этом случае, конечно, её всегда можно каррировать (как уже показано выше), либо же можно воспользоваться синтаксисом “с подчёркиваниями” и получить тот же результат:
  def add(x: Int, y: Int) = x + y   // (Int, Int) => Int

  val add7 : Int => Int = add(7, _) // Int => Int
  def add5 = add(5, _: Int)         // Int => Int
Выглядит, конечно, не так красиво, как в случае с каррированным вариантом, и чуть более ограничено (представьте, что каррируется не 2, а больше параметров), но вполне читаемо и работоспособно :)

Частичное применение так же очень часто используется в качестве “вложенных” функций:
  def highlight(context: String) = {
    def find = findElement(context) _
    
    val title = find("td.title")
    val price = find("td.price")
    
    ???
  } 
Функция “highlight” в данном случае получает на вход контекст и сама конструирует внутри себя вспомогательную функцию “find", способную искать в заданном контексте, после чего многократно использует эту вспомогательную функцию вместо того, чтобы “засорять” код “прямым” вызовом “findElement”.

В C# для того, чтобы “частично применить” метод нужно фактически полностью переписать сигнатуру функции (написать новую функцию с теми же парамерами за исключением “частично применяемых”) и вызвать более полный метод из “частично применённого”.
В качестве аналога вложенных функций в C# можно использовать переменные типа Func<…>, что почти работоспособно (кроме случаев с анонимными типами), но синтаксически выглядит не очень красиво.

“Позови меня тихо по имени..."


В Scala принята следующая терминология:
- "обычная" передача параметров (times: Int) называется передачей “по значению” (by value), поскольку значение, как обычно, вычисляется при передаче в функцию.
- передача параметров с помощью знака “=>” (body: => T) называется передачей “по имени” (by name). Значение body в этом случае будет вычисляться каждый раз когда происходит обращение к body, либо не будет вычислено ни разу, если обращения к body не произойдёт.

  def onlyIf[T](condition: Boolean)(f: => T) : Option[T] = {
    if (condition) Some(f)
    else None
  }

  val discount = onlyIf(order.amount > 100) {
    //сложный процесс высчитывания скидки будет запущен только в случае выполнения условия 
  }

Лично мне кажется, что такая терминология является несколько запутывающей. В действительности параметр “по имени” является ничем иным, как функцией, не принимающей параметров и возвращающей результат (что не имеет к “имени” практически никакого отношения). Если вместо термина “по имени” просто смотреть на такой параметр как на функцию, то и поведение становится логичным и понятным.

В параметр “по имени” можно передавать не только функцию или блок { … } (который по сути тоже является функцией), но и “обычные” значения. Действительно, любое значение может быть рассмотрено как функция (добро пожаловать в функциональный язык!), не принимающая никаких параметров и возвращающая это значение.
То есть, выражение retry(5)(“alexey”) будет скомпилировано, исполнено и результатом будет являться строка “alexey”.

Заключение


В этой заметке мы рассмотрели синтаксис функций и базовые аспекты работы с ними. В качестве заключительного примера давайте попробуем восполнить “пробел” в Scala и написать простую конструкцию, аналогичную конструкции using в С# (в Scala нет “синтаксического сахара” такого рода):
  trait Closable {
    def close()
  }

  def using[T, K](factory: => T with Closable)(block: T => K) = {
    val resource = factory
    try {
      block(resource)
    } finally {
      resource.close()
    }
  }
Трейт “Closable” (о трейтах мы подробно поговорим в следующей заметке, а пока будем считать, что это синоним interface в C#) определяет метод “close”, отвечающий за освобождение ресурса (закрытие файла, соединения с БД, сокета и т.д.)

Как и using в C# Функция “using” принимает экземпляр, который “реализует” “Closable”. Передача параметра осуществляется “по имени”, иными словами в функцию передаётся не сам экземпляр, а функция, возвращающая экземпляр, поэтому я назвал параметр factory.
Вторым параметром “using” принимает функцию, принимающую исходный экземпляр и возвращающую какой-то результат.
В процессе своей работы функция “using” получает экземпляр ресурса, выполняет функцию для этого ресурса и закрывает ресурс вызывая метод “close”, возвращая результат работы блока.

Использовать теперь эту функциональность можно так:
  val result = using(new MyFileReader()) { reader =>
    reader.readAllLines()
  }
Фактически мы написали аналог using из C# в несколько строк. Но при этом наш самописный вариант оказался лучше! :)
Лучше и удобнее он потому, что в отличие от C#, где using это просто конструкция (statement), наш вариант способен вернуть значение того, что происходит внутри блока! Ура, больше не нужно “бороться” с видимостью переменных, определяя их за пределами using и инициализируя из значениями “по умолчанию” только для того, чтобы изменить их значение внутри блока! В случае нашего варианта код выглядит гораздо чище.
Кроме того, первый параметр (factory) мы объявили как параметр “по имени”, что тоже даёт нам определённую свободу: мы уже внутри  функции можем принять решение о том, в какой момент инициировать создание экземпляра и при этом это будет совершенно прозрачно для вызывающей стороны.

Ух! Всё! Это было долго, но я надеюсь, что синтаксис и варианты применения функций в Scala теперь будут более понятны при чтении  и написании кода.

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

Хороших выходных!
4/30/2014 12:54:30 PM
В предыдущей заметке мы рассмотрели создание типов в Scala а так же такие особенности языка, как объект-спутник и метод “apply”.
Сегодня мы рассмотрим аспекты Scala, аналогов которых, к моему огромному сожалению, нет в C#: сопоставление по образцу и кейс-классы.
ОК, в F# есть discriminated unions, которые тоже очень удобны и которые, возможно, даже лучше выглядят синтаксически, но в C# нет ничего похожего и, скорее всего, не будет.  

Кейс-классы (case classes)

Мы уже видели насколько компактным и красивым является описание класса в Scala с помощью синтаксиса “конструктора по умолчанию” и параметров с модификаторами “val” или “var”. А использование объекта-спутника с методом “apply” делает возможным избежать ключевого слова “new” для создания экземпляра, что делает код ещё более компактным и красивым.

Добиться подобных “красивостей” в Scala можно ещё одним способом: определением кейс-класса:
case class Rectangle(length: Int, width: Int) {
  def area = length * width
}
Основное синтаксическое отличие от предыдущего примера состоит в том, что мы добавили ключевое слово “case” при декларации класса и убрали “val” при декларации параметров конструктора.
В случае кейс-класса компилятор сам сгенерирует публично доступные поля/свойства класса для всех параметров, переданных в конструктор. Кроме этого компилятор сам сгенерирует объект-спутник, в котором (помимо прочего) будет реализован метод “apply”, являющийся фабрикой для данного типа (так, что мы можем создавать экземпляры без использования ключевого слова “new”).
Кроме этого компилятор сгенерирует ещё ряд полезных штук, например метод “equals” и оператор "==“ будут “правильно” сравнивать экземпляры (два экземпляра равны если равны все е��о свойства, переданные в конструктор), методы “hashCode” и “toString”.
Представьте, сколько кода пришлось бы написать в C# для того, чтобы добиться такого результата (хотя F# справляется с этой задачей “на ура” не хуже Scala).

Кейс-классы нередко используются в Scala для задания discriminated unions:
trait Shape {}
case class Rectangle(length: Int, width: Int) extends Shape
case class Circle(radius: Int) extends Shape
case class Triangle(a: Int, b: Int, c: Int) extends Shape
Классы “Rectangle”, “Circle” и “Triangle” являются фигурами (“наследуют” “Shape”). Иными словами фигура может быть представлена прямоугольником, кругом либо треугольником и ничем иным, что и формирует discriminated union.
С большой натяжкой можно сказать, что discriminated union - это такой “улучшенный” enum, который строго описывает набор вариантов.
Не обращайте пока внимания на “trait”, давайте пока представим, что это аналог “interface” из C#, а с трейтами разберёмся в части 5. 

Так же кейс-классы очень часто используются для создания типизированных объектов-значений (Value Objects):
case class UserId(id: Int)
case class CompanyId(id: Int)
case class FirstName(value: String)
case class LastName(value: String)
def addUserToCompany(user: UserId, company: CompanyId) = ???
def registerUser(firstName: FirstName, lastName: LastName) = ???
Вообще-то подобная практика рекомендуется и в других языках, включая C#, но поскольку создание класса в C# является “целым делом” (во всяком случае не одной простой строчкой, не говоря уже об определении методов сравнения, хешкодов и т.д.), то и делают программисты это довольно редко.

Но ведь, согласитесь, строгая типизация нам дана не для того, чтобы мы потом путали местами параметры функций или использовали везде примитивные типы, или имели возможность по ошибке “запихнуть” фамилию в поле “место рождения”, или могли использовать идентификатор пользователя вместо идентификатора компании! Пусть компилятор следит за этим :)
В Scala использование Value Objects - дело обычное и повсеместное.

Помимо “бесплатной” реализации операций сравнения, хешкодов, toString, companion object и apply есть и ещё один огромный плюс в использовании кейс-классов: возможность сопоставления с образцом.

Сопоставление с образцом (pattern matching)

Возможность сопоставления с образцом присуствует во многих функциональных языках (включая F#) и Scala - не исключение. Для тех, кто не сталкивался с такой возможностью, наверное, будет проще показать на примере, чем объяснить словами:
  def area(shape: Shape) : Double =
    shape match {
      case Rectangle(l, w) => l * w
      case Circle(r) => math.Pi * math.pow(r, 2)
      case Triangle(a, b, c) =>
        val p = (a + b + c) / 2
        math.sqrt(p * (p - a) * (p - b) * (p - c))
    }
Функция “area” в данном примере для вычисления площади переданной фигуры сопоставляет её с образцами (ключевое слово “match”), один за одним. Для первого же “совпавшего” же образца будет вычислено выражение, идущее после знака => и значение этого выражения будет являться значением всего выражения “match”, которое и возвращает функция “area”.
Заметьте, что в случае “совпадения” с образцом, например, “Rectangle”, l и w получат значения length и width прямоугольника, то есть, мы как-бы “достаём” эти значения из переданного экземпляра в процессе сопоставления с образцом и можем использовать их как-то в дальнейшем (вычислить площадь).
В C# для реализации подобной функциональности нужно было бы написать что-то вроде “если shape является прямоугольником, то привести shape к прямоугольнику, присвоить переменным l и w значения length и width этого прямоугольника и произвести действия над ними, иначе если это круг, то….”. Сопоставление с образцом позволяет чётко и просто выразить именно то, что имеет смысл, декларативно описать ситуацию.
Иными словами мы можем сказать, что сопоставление с образцом - это сильно улучшенный и расширенный вариант switch … case.

Сопоставление с образцом используется не только для определения типа, оно может использоваться и для множества других целей. Например:
case class Person(firstName: String, lastName: String)
// ...
def greeting(p: Person) = p match {
  case Person("Alexey", _) => "Hi, Alexey!"
  case Person(_, surname) => s"Hello, Mr. $surname"
}
Здесь функция “greeting” возвращает приветствие для экземпляра “Person”. Для этого параметр сопоставляется с образцом, и в случае, если свойство firstName равно “Alexey”, возвращает фамильярное приветствие, в противном же случае официально приветствует человека по его фамилии.
Символ _ (подчёркивание) в случае сопоставления с образцом означает “что угодно”, например в первом случае нас не интересует фамилия человека, а во втором - имя.
В “официальном” случае приветствие конструируется с использованием т.н. “интерполяции строк” (string interpolation), которая есть в Scala (и многих других языках), но отсутствует в C#. Я не буду заострять внимание на этой возможности, думаю, что из примера всё должно быть понятно.

Очень часто сопоставление с образцом используется для “проверки” некой структуры, например:
lst match {
  case List() => println("empty")
  case List(_, 2, _, 4) => println("list with 2 ends with 4")
  case h :: 2 :: _ :: tail => println(s"list starts with $h and ends with $tail")
  case _ => println("something I don't understand")
}
В случае пустого списка данная конструкция напечатает текст “empty list”.
В случае списка, состоящего ровно из 4-х элементов, где второй элемент равен 2, а 4-й равен 4 будет напечатано “list with 2 ends with 4”, причём первый и третий элементы могут быть любыми.
В случае списка любой длины более 3 элементов, в котором второй элемент равен 2, будет напечатан текст, содержащий значение первого элемента списка (оно будет находиться в h) и весь оставшийся список начиная с 4-го элемента (остаток списка будет находиться в tail).
В любом другом случае выражение напечатает “something I don’t understand”.

Синтаксис с двоеточиями - не “синтаксический сахар” для списков в Scala. Scala даёт возможность определять собственные операторы и оператор :: уже определён для списков и позволяет наглядно конструировать списки. Мы можем создавать разные операторы и для других целей, хотя Scala рекомендует это делать только в том случае, когда синтаксис оператора интуитивно понятен читающему. В противном случае рекомендуется использовать методы.

Кстати говоря, обработка исключений в Scala тоже производится с помощью сопоставления с образцом:
  def getS3File(file: String) : Option[S3File] = {
    try {
      Some(downloadFile(file))
    } catch {
      case e: AmazonS3Exception if e.getStatusCode == 404 => None
      case _: AmazonS3Exception => getS3File(file)
      case NonFatal(_) => None
    }
  }
Функция “getS3File” скачивает файл с Amazon S3 и возвращает результат. В случае если возникает исключение типа “файл не найден" (AmazonS3Exception с установленным статусом 404) функция функция возвращает None. В любых других случаях AmazonS3Exception функция будет пытаться повторить попытку скачивания. Если же исключение не является AmazonS3Exception, и при этом не является “фатальным” (к фатальным относятся исключения типа StackOverflow, NotImplemented и т.д.) функция тоже вернёт None. Случай фатальных исключений не описан, следовательно, такое исключение не будет перехвачено.
Обратите внимание на конструкцию NonFatal(_). Будучи интуитивно понятной, сама по себе она не является исключением. Вместо этого она тоже является “образцом”, с которым сопоставляется возникшее исключение. Как это делается? Смотрите чуть ниже :)

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

Довольно магии, как это работает?

Действительно, давайте разберёмся с тем, как именно Scala определяет совпадение с образцом. Можете пропустить этот раздел если не хотите чуть-чуть покопаться в деталях :)

Возможность сопоставления экземпляра с образцом - не магия. Хотя для кейс-классов сопоставление с образцом по структуре класса компилятор берёт на себя, в других, более интересных случаях мы можем “вручную”, создавать интересующие нас “образцы".
Вся магия сопоставления с образцом кроется в методе “unapply”, который, в противоположность методу “apply” называется “распаковщиком” или “экстрактором” (extractor).
Давайте определим класс “Circle” не как кейс-класс, а как “обычный” класс. В этом случае компилятор не будет генерировать объекта-спутника и методов “apply” и “unapply”, но мы можем сделать это сами:
class Circle(val radius: Int)
object Circle {
  def apply(r: Int) = new Circle(r)
  def unapply(c: Circle) = Some(c.radius)
}

Метод “apply” мы уже рассматривали в прошлый раз.
Интересующий нас метод “unapply” принимает экземпляр класса “Circle” и “распаковывает” его на “составляющие”. В нашем случае “составляющая” всего одна: радиус (в случае нескольких значений “unapply” вернул бы их в в виде tuple).

OK, “unapply” возвращает не просто c.radius, а некий Some(c.radius), а значит как раз время для “лирического отступления" :)
Как и во многих других функциональных языках, в Scala определён тип Option[T], который может принимать одну из двух форм: либо Some(value: T), либо None. Используется этот тип, как и следует из названия, когда некое значение опционально: либо оно есть, либо его по какой-то причине нет.
В императивных языках (включая C#) в качестве “отсутствующего результата" часто используется null, но мы все знаем как это неудобно: в одних случаях это чревато NullReferenceException в разных неожиданных местах, в других непонятно, толи null - это действительное значение, толи это означает отсутствие значения. Плюс к этому компилятор не может помочь в случае null, поэтому мы вынуждены делать многочисленные проверки на null, которые по-хорошему нужно делать везде и которые часто делать забывают и т.д.
А в случае, когда нам нужно показать отсутствие, например, численного значения… мы либо “договариваемся” использовать “магические числа” вроде “-1”, либо (OMG) начинаем манипулировать значениями типа Nullable<Int>.

В функциональных языках (F#, Scala) в таких случаях используется тип Option. В случае, когда значение есть, выражение возвращает Some(value), в противном случае оно возвращает None. Это помогает избежать путаницы с семантикой, проблем с NullReferenceException и избежать многочисленных проверок.
Мы обязательно поговорим о вариантах использования Option в одной из следующих заметок, предположительно в когда речь пойдёт об основах функциональной парадигмы Scala.

Итак, при сопоставлении с образцом в Scala просто вызывается метод “unapply”. Всё просто и никакой магии :)
Тип возвращаемого “unapply” значения - Option[T]. Если “unapply” возвращает Some(value), то “распаковка” считается успешной, ветка (case) проверяет соответствие “распакованных” значений и устанавливает значения.
Если же “unapply” возвращает None, то это означает, что совпадения нет. Тогда происходит тестирование следующей ветки и т.д.
Как видите, никакой магии :)

Как я уже сказал, в случае кейс-классов компилятор генерирует метод “unapply” наряду с другими полезными методами, такими, как “apply”, “toString”, “hashCode”, “equals” и др.
Однако, определение методов “unapply”, содержащих какую-то дополнительную логику по “распаковке” и возвращающих такие “распакованные” данные позволяет создавать элегантные решения, использующие сопоставление с образцом.

Так, например, теперь даже не глядя в исходный код становится понятно как работает NonFatal(e) из предыдущего примера. На самом деле NonFatal не имеет никакой специфики относительно перехвата исключений. Очевидно, что это всего лишь некий объект, в котором определён метод “unapply(e: Throwable)”, принимающий исключение в качестве параметра. В случае, если исключение не является фатальным, этот метод возвращает Some(e), в противном случае он возвращает None.

Регулярные выражения в Scala тоже пользуются возможностью определения “unapply”, позволяющего сопоставлять результат с образцом:
val fooBar = "^(\\w+).*(foo|bar).*".r

val essence = "Simon would like you to foo for him" match {
  case fooBar(name, msg) => s"$name: $msg!"
  case _ => "I don't understand"
}
В результате значение essenсe будет равно “Simon: foo!”

А в качестве примера самостоятельной реализации метода “unapply” можно рассмотреть наивную (для простоты примера) функциональность парсинга адреса электронной почты:
object Email {
  def apply(user: String, domain: String) = user +"@"+ domain
  def unapply(str: String) = {
    val parts = str.split("@")
    if (parts.length == 2) Some(parts(0), parts(1)) else None
  }
}

object Test extends App {

  val email = Email("alexey", "downunder.co.au")

  val printableEmail = email match {
    case Email(name, server) => s"$name (AT) $server"
    case _ => "< no email >"
  }}
В результате исполнения этого кода значение email будет иметь тип String и будет равно “alexey@downunder.co.au”, a значение printableEmail будет равно “alexey (AT) downunder.co.au”.
Если бы значение email не было корректным (не удовлетворяло бы правилам, описанным в “unapply”), то значение printableEmail было бы равным “< no email >”.

Заключение

Сопоставление с образцом хоть и может быть классифицировано как “синтаксический сахар”, является одним из наиболее часто используемых инструментов в языках, его поддерживающих потому, что позволяет “декларативно” описать “намерения” кода вместо того, чтобы углубляться в детали реализации (как это было бы в случае с if .. else).
Программируя на функциональных языках (таких, как Scala, F#, Haskell и другие) вы будете сталкиваться с этим чуть чаще, чем постоянно. В следующий раз, разбирая свойства классов с помощью “if” на C# подумайте о том, насколько было бы проще, если бы вы могли воспользоваться возможностью сопоставления с образцом :)

Лично для меня pattern matching и discriminated unions являются двумя наиболее важными возможностями, которые отсутствуют в C# и которые я очень сильно хотел бы там видеть. Scala предоставляет обе эти возможности.

В следующей заметке мы более подробно рассмотрим синтаксис и возмоности функций и методов, и это, похоже, будет “долгий разговор” :)

4/28/2014 3:13:08 PM
В предыдущей заметке мы говорили о том, что такое Scala и какие инструменты нужны для того, чтобы поэкспериментировать с этим интересным языком. Так же мы рассмотрели пример “Hello World”, хоть и без углубления в “механику” происходящего.
На самом деле пример с “Hello World” не так прост из-за использования App, с которым связана некая “магия”, но не будем сейчас об этом, быть может вернёмся к магии отдельно позже.

Сегодня мы рассмотрим базовые аспекты работы с типами.

Объявления типов


Для объявления типа в Scala используются ключевые слова class и object:
class Rectangle(l: Int, w: Int) {
  val length = l
  val width = w

  def area = { l * w }
}

object Dot {
  def area = 0L
}
Класс “Rectangle” принимает в качестве параметров конструктора значения “l” и “w” (длина и ширина), которые доступны внутри класса. Такое объявление с указанием параметров конструктора после имени класса называется “конструктором по умолчанию” (default constructor) и выражения, следующие далее в фигурных скобках “исполняются” внутри конструктора экземпляра.

Ключевое слово “val” определяет свойство типа Rectangle, причём это свойство является неизменяемым (immutable). Это означает, что изменить однажды полученное значение компилятор уже не даст.
Изменяемые значения в Scala могут быть объявлены с помощью ключевого слова “var” (почти как в C#).
Значения, декларируемые с помощью “val” называются значениями (values), а декларируемые с помощью “var” - переменными (variables).

Значения length и width определены значениями параметров, переданных в конструктор.
Точки с запятой в конце каждой строки в Scala опциональны и не рекомендуются. Точки с запятой нужны только в том случае, если мы хотим написать несколько инструкций в одной строке.

Идеоматически в Scala (и других функциональных языках) предполагается отдавать предпочтение неизменяемым (immutable) конструкциям, соответственно, использование переменных является крайне редким. Считается, что нужно иметь веские основания для того, чтобы использовать “var” в своём коде, а использование изменяемого состояния часто считается плохим дизайном.

Метод “area” определён как произведение длины и ширины.  Ключевое слово “def” используется для объявления функций. Метод “area” не принимает параметров и в данном случае использование пустых круглых скобок после имени метода не обязательно (в Scala считается "хорошим тоном” не декларировать пустых круглых скобок без необходимости).
Типы параметров функций в Scala указывать обязательно, а вот тип возвращаемого значения в большинстве случаев компилятор способен вывести сам.
Поскольку Scala - функциональный язык и функции являются “гражданами первого класса” (first class citizens), как и любые другие значения (об этом ещё поговорим) то и семантика объявления функции схожая: сигнатура функции “равна” некому выражению, по аналогии с уравнениями в математике (моделями чего функции, в общем-то, и являются).

Одним из основных отличий от описания функций в C# здесь является отсутствие необходимости в ключевом слове “return” для возврата значения из функции. Как и в большинстве функциональных языков, последнее вычисленное функцией значение является значением, которое она возвращает. В нашем случае с “area” это выражение единственное: произведение длины и ширины прямоугольника. На это можно смотреть так: функция что-то делает-делает, и то, что в результате остаётся и является её возвращаемым значением.

К слову о выражениях. В Scala практически всё (за исключением, пожалуй, определения функций, значений и переменных) является выражением и возвращает результат. Например, конструкция if … else … в Scala, в отличие от C#, является не просто конструкцией ветвления, а выражением:
val result = if (someValue < 0) 0 else someValue
То же самое можно касается и других конструкций, например trycatch … и т.д: все они возвращают значения.

Тип “Dot", в свою очередь, объявлен как object. Пример использования:
val dotArea = Dot.area
Хотя выглядит это так, как будто “Dot” является статическим классом, на самом деле это не так. “Dot” является синглтоном и в системе будет существовать только один экземпляр данного типа.
Типы, объявленные как object, пожалуй, наиболее близкий аналог статических классов C# семантически, но они более привлекательны, так как обладают ещё и свойствами “обычных” экземпляров (например, статический класс нельзя было бы передать в функцию в качестве параметра, а синглтон-объект можно).

Теперь, когда мы примерно понимаем, что происходит в приведённом выше коде, мы можем его несколько “оптимизировать” его с точки зрения читабельности. Scala старается (хотя и не всегда получается) по возможности сократить количество всевозможных “церемоний” в виде нагромождения ключевых слов и сделать код максимально простым для чтения.

Так, мы можем переписать класс “Rectangle” в следующем виде:
class Rectangle(val length: Int, val width: Int) {
  def area = length * width
}
Отличие от предыдущего варианта состоит в том, что параметры конструктора класса объявлены с использованием ключевого слова “val”. В этом случае length и width будут публично доступны как поля/свойства класса “Rectangle” и нам больше не нужно дублировать декларацию и инициализацию этих полей.
Так же можно использовать “var”, но, мы уже договорились использовать переменные только в самом крайнем случае :)
Кроме того я убрал фигурные скобки из функции “area”: они необходимы только тогда, когда тело функции состоит из более чем одного выражения.

Ещё одна небольшая модификация:
class Rectangle(val length: Int, val width: Int) {
  lazy val area = length * width
}
Действительно, поскольку значения length и width неизменяемы, пересчитывать area каждый раз, получая одно и то же значение, бессмысленно. Однако и высчитывать его заблаговременно тоже может не иметь смысла, так как значение площади может никогда не понадобиться.
Использование ключевого слова “lazy” позволяет решить эту “проблему”. В случае использования “lazy” значение area будет посчитано один раз только тогда, когда оно понадобится.
Это сравнимо с типом Lazy<T> в С# с тем исключением, что нам не нужно возиться с дополнительным типом, Scala сделает всё за нас :)

Companion Objects и метод “apply”


Говоря о типах Scala нужно сказать о ещё одном аспекте, аналога которому в C# не существует: объекты-спутники (companion object).
Companion Object - это объект (object) имеющий то же имя, что и класс. Например, для класса Rectangle” так же может быть определён спутник object Rectangle.
Связь класса с объектом-спутником является одной из самых сильных в Scala: класс имеет возможность “видеть” приватные методы своего объекта-спутника.
С точки зрения C# здесь можно провести аналогию со статическими методами. В Scala отсутствует возможность объявить статический метод, но объявив метод в объекте-спутнике мы семантически получаем тот же эффект.

Кроме того объекты-спутники часто используются как фабрики для создания экземпляров своего класса-спутника:
class Rectangle(val length: Int, val width: Int) {
  lazy val area = length * width
}

object Rectangle {
  def square(size: Int) = new Rectangle(size, size)
  def apply(length: Int, width: Int) = new Rectangle(length, width)
}
Метод “square” объекта-спутника конструирует квадрат заданного размера.

Метод “apply” является просто фабрикой для создания экземпляра “Rectangle”.
apply” в Scala особый метод: “синтаксический сахар” языка позволяет “заменить” вызов метода “apply” круглыми скобками.
object Test extends App {
  val norm = new Rectangle(1, 1)
  val bigSquare = Rectangle.square(100)

  val screen = Rectangle(16, 10)
}
Значение norm задаётся с помощью прямого вызова конструктора класса “Rectangle”.
Значение bigSquare получается путём вызова метода объекта-синглтона (семантического “аналога” статического класса/метода C#)
Значение screen получается путём вызова метода “apply” объекта-синглтона. Это и есть тот самый “синтаксический сахар”: вместо явного указания вызова метода “apply” Scala позволяет просто использовать круглые скобки, позволяя типу выглядеть так, как будто он является функцией (технически всё происходит наоборот: функция в Scala это тип для которого определена операция “apply).

Поскольку такой синтаксис очень удобен, метод “apply” очень часто используется в качестве метода-фабрики. В этом случае мы как минимум можем опустить некрасивый вызов операции “new” и сделать наш код более читабельным.

Конечно, метод “apply” может быть определён для любого класса, не обязательно для объектов или объектов-спутников и не обязательно в качестве фабрики. В любом случае, когда мы хотим, чтобы вызов выглядел “красиво” как функция мы можем добавить в класс метод “apply”.

Например, так можно реализовать то, что называется индексатором (indexer) в C#:
  val element = myList(5)
  val value = myMap("key")
Получение 5-го элемента списка или получение значения по ключу на самом деле является всего лишь вызовом метода “apply”. Опять же, индексаторами дело не ограничивается: мы можем создавать классы-операции, которые будут “красиво” смотреться в коде, или использовать “синтаксический сахар” метода “apply” как-то ещё.

Программисты на C++ могут называть такие классы-операции “функторами” потому, что у них “переопределён оператор 'круглые скобки’”, однако нужно понимать, что такая конструкция в C++ названа “функтором”, видимо, по недоразумению и ничего общего с термином "функтор" не имеет. Я бы не советовал так использовать этот термин за пределами С++, поскольку термин “функтор” имеет вполне определённое значение и используется в (функциональном) программировании в его “истинном” смысле. Возможно в дальшейшем в этой серии мы дойдём и до функторов :)

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

В следующей заметке мы рассмотрим некоторые аспекты Scala, которые, к сожалению, напрочь отсутствуют в C#, например сопоставление по образцу (pattern matching)
4/26/2014 4:01:08 PM

Поскольку в последнее время основным моим языком программирования является Scala я подумал о том, чтобы написать небольшую серию заметок на тему “Scala глазами C#-разработчика”. Основная цель серии – дать C#-разработчику представление о Scala. Предполагаемая аудитория – C#-разработчики, не встречавшиеся ранее со Scala, либо что-то отдалённо слышавшие об этом языке.
Первая заметка будет “вступительной”, но в последующих мы будем всё внимание уделять коду на Scala. Я попытаюсь структурировать каждую заметку так, чтобы она была посвящена какому-то аспекту и так, чтобы “сложность” материала возрастала от начала заметки к концу. При этом я не планирую залезать слишком далеко в “дебри”, оставаясь на “прикладном” уровне.
Некоторые вещи я буду объяснять подробно, некоторые – не буду совсем (особенно те, о значении которых можно просто догадаться из синтаксиса).
Давайте попробуем.

Что такое Scala?

Проект Scala был начат 11 лет профессором Мартином Одерски, который позже основал компанию TypeSafe, занимающуюся поддержкой и развитием Scala и “сопутствующих” библиотек и фреймворков (таких, как Play, Akka и т.д.).

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

Почему Scala?

А почему бы и нет? Изучение различных языков и различных парадигм очень полезно для профессии: программист должен быть “полиглотом”.
И Scala очень хорошо подходит для того, чтобы стать “следующим языком” по нескольким причинам, например:

  1. Scala позволяет выйти за пределы “.NET пузыря”, за которым, на самом деле, существует целый мир :)
  2. Scala – функциональный язык (functional language), что означает шанс ближе познакомиться с функциональным программированием.
  3. Scala так же объектно-ориентированный язык, что означает возможность применения имеющихся знаний и более лёгкий “порог вхождения” (по сравнению, скажем, с Haskell)
  4. Scala имеет очень мощную и гибкую систему типов, гораздо мощнее и шире, чем то, что мы имеем в случае с С# (или F#, если уж на то пошло). Стоит сказать, что Microsoft одно время занималось спонсированием работ, позволяющих использовать Scala “поверх” .NET-платформы, однако оказалось, что система типов .NET CLR слишком ограничена и недостаточно гибка для того, чтобы можно было реализовать Scala.NET, поэтому работы были свёрнуты.
  5. Одно только наличие Akka (актор-ориентированного фреймворка) делает изучение Scala чуть ли не обязательным ;)
  6. Scala – довольно красивый и элегантный язык, работать с которым легко и приятно :)

Инструменты

Для того, чтобы работать со Scala вам понадобится:

  1. Собственно, Scala. Скачать последнюю версию (под любую платформу) всегда можно на оффициальном сайте: http://www.scala-lang.org/download/
  2. Средства разработки. Здесь у вас есть выбор:
    1. Scala IDE (http://scala-ide.org/). Довольно проста в использовании, НО: сделана на основе Eclipse со всеми вытекающими последствиями. Если вы вдруг любитель Eclipse – то пожалуйста.
    2. Intellij Idea (http://www.jetbrains.com/idea/download/). Разработана JetBrains (те ми же ребятами, что делают ReSharper) и является, наверное, наиболее продвинутой IDE среди всех существующих. Можно долго и упорно спорить о том, кто же более продвинут: Visual Studio (с установленным решарпером) или же Idea, но для разработчиков на C# в Visual Studio само наличие таких споров говорит о многом ;) Для работы со Scala достаточно Community Edition (которая бесплатна). После установки Idea нужно будет добавить плагин Scala.
    3. Sublime Text. Если вы уже используете этот замечательный редактор (а если нет – по почему?! нужно срочно скачать и посмотреть “курс молодого бойца”, который расскажет о многих замечательных возможностях), то остаётся только установить плагин SublimeREPL. Как следует из названия, плагин даёт возможность использовать REPL (Read–eval–print loop) для очень многих языков (включая F# и Scala).
      Для тех, кто не знаком с термином REPL поясню так: вы пишете 2+2, нажимаете “ввод”, получаете 4. Это можно делать и просто в консоли запустив “scala”, но в Sublime Text это делать гораздо приятнее: подсветка синтаксиса и прочие полезности.

Из перечисленного я лично использую Idea для работы и Sublime Text REPL для быстрых экспериментов.

Hello World

Существует добрая традиция “hello world”, обходить которую было бы моветоном :)
”Hello World” на Scala может выглядеть следующим образом:

1
2
3
object HelloWorld extends App {
    println("Hello World")
}

HelloWorld объявлен как object, в Scala это означает, что для данного типа будет создан единственный экземпляр, иными словами HelloWorld является синглтоном (singleton). Тип HelloWorld так же “расширяет” (для аналогии с C# в первом приближении можно сказать, что “наследует”) некую сущность App (базовый тип, содержащий входную точку “main”).  Мы могли бы не использовать App вообще, а вместо этого определить метод “main” самостоятельно, однако с использованием App код выглядит красивее и понятнее, а главное – его, кода, меньше! ;) 
В результате исполнения данного кода на экране появится знакомое приветствие.

Нужно обратить внимание на то, что HelloWorld является именно синглтоном, а не статическим типом (не аналогом public static class в C#)! В Scala нет понятия статических классов или статических методов.

С практической точки зрения это означает, что:

  1. Ура, сиглтоны делаются легко и просто, не нужно создавать никаких статических методов вроде Instance { get; }, не нужно делать никаких двойных проверок с блокировками и т.д.
  2. Как и в случае со статическим типом среда берёт на себя ответственность за создание и поддержку экземпляра. Программист всегда может обратиться к объекту по имени типа и вызывать какой-либо метод: HelloWorld.hashCode()
  3. В отличие от статического типа экземпляр можно использовать как любой другой экземпляр любого другого типа, например передать его в качестве параметра функции: takeThis(HelloWorld), вернуть в качестве значения функции или присвоить какой-либо переменной.

В следующей заметке мы разберёмся с этим подробнее и я расскажу о базовом синтаксисе Scala и о различных вариантах определения типов.

2/9/2014 6:25:08 AM

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

Я плотно работал с TFS фактически с момента его появления (с 2005-й версии и до версии 2010-й) не только как разработчик, мне приходилось и устанавливать, и администрировать, и заниматься правкой шаблонов проектов, и настраивать CI.
Я осуществлял две такие миграции “с TFS на что-то ещё” и здесь я попытаюсь рассказать почему, как и что.

Давайте сначала посмотрим, что представляет из себя TFS.
TFS – это набор инструментов, включающий в себя систему контроля версий, систему управления задачами, билд-систему и систему тестовых окружений (TestLabs). Эти системы интегрированы в единую экосистему, всё это интегрируется c Visual Studio и работает практически “из коробки”.

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

Source Control

Система контроля версий в TFS централизована. Это означает, что у вас в компании (или “в облаке” в случае 2012-й версии) есть некий “центральный сервер”, с которым работают разработчики. С точки зрения системы контроля версий работа сводится к операциям CheckOut (взять файл для правки) CheckIn (“загрузить” исправленный файл) и операциям по работе с бранчами.

TFS в своей работе опирается на Read Only атрибут файлов в локальной файловой системе на машинах разработчиков: при CheckOut этот атрибут с файла снимается, а при CheckIn ставится обратно. Казалось бы – мелочи, но это означает, что для того, чтобы начать править файл, нужно связаться с сервером и сообщить ему об этом. При этом сервер сам ведёт реестр файлов и их статусов для каждого компьютера каждого разработчика. Конечно, можно вручную снять атрибут и поправить файл если сервер, например, недоступен, но это означает, что позже придётся “объясняться” с TFS на предмет того, что же тут происходит.
Существует ещё возможность временно “уйти в оффлайн”, продолжать работать, а потом снова “выйти в онлайн”, и TFS сумеет обнаружить изменения и позволит сделать CheckIn, но это означает, что на период работы “оффлайн” разработчик начисто лишается системы контроля версий, работая лишь с кучкой файлов на диске. Ни откатить что-то на момент “как было”, ни посмотреть историю изменений, да и вообще никаких плюсов системы контроля версий в этом режиме не предусмотрено. Её нет на этот период – системы контроля версий.
При переходе обратно в режим “онлайн” локальные файлы будут проанализированы системой и TFS предложит “объясниться” с ней на предмет того, что и как изменилось. “Объяснения” эти бывают не из лёгких, особенно в случае обнаружения конфликтов.

Бранчи в TFS тоже создаются на центральном сервере. Локально каждый бранч представляет собой отдельную папку на диске с копией файлов репозитория. Если разработчик хочет сделать ветку, ему нужно создать бранч на сервере и скачать его себе на локальный диск. Разработчик, таким образом, не может быстренько “на минуточку” создать для себя ветку что-то попробовать, поэкспериментировать и т.д. Всё, что связано с бранчами – это серьёзная церемония, в связи с наличием которой разработчики, использующие TFS бранчами практически не пользуются. Это просто неудобно, а слияние бранчей настолько плохо, что ещё и требует недюжей аккуратности и запаса нервов. В связи с этим, разговаривая с командой, использующей TFS на предмет бранчей, часто можно столкнуться с парадоксом Блаба по отношению к бранчам (или ещё можно сказать “парадоксом фаната Apple”: “а мы это не пользуем”, “а нам это и не надо” и т.д.). На самом деле это происходит не потому, что “не надо”, а потому, что инструменты настолько плохи, а возможности настолько неудобны, что использование их делается нецелесообразным.

Давайте сравним это, например, с популярной системой контроля версий Git.  О Git напимано много и я не буду описывать принципы его работы, скажу лишь о разнице по отношению к TFS.

В случае с Git отсутствует т.н. “центральный репозиторий”. Каждый разработчик на своём локальном компьютере имеет собственную копию репозитория. Поэтому разработчик никогда не остаётся без системы контроля версий, даже если он полностью изолирован от всего остального мира, он может продолжать работать, иметь доступ к истории, делать вносить изменения и добавлять ревизии в систему контроля версий.
Не нужно понимать это неправильно: скорее всего в компании будет некий сервер с неким репозиторием, который будет назван “центральным” и который будет считаться “источником правды” (source of truth). Но это назначение – логическое, а не физическое. В топологии Git это всего лишь ещё один репозиторий, который люди “договорились” считать главным. Физически разработчик может равно получить код как с “главного” репозитория, так и с репозитория своего коллеги или откуда-нибудь ещё.
Убивающе сильной стороной Git является работа с бранчами. Поскольку каждый разработчик имеет свою собственную копию репозитория, он может создавать бранчи локально. Причём создание бранча не требует создания новой копии в отдельной папке и т.д, это совершенно не занимает времени (милисекунды) и не требует никаких усилий. Становится более удобной парадигма работы: “а ну-ка я попробую так” – переключился в бранч – поработал – не понравилось – переключился обратно – понравилось – слил с “основным” своим брачем и т.д.
Для тех, кто читает об этом впервые, парадокс Блаба будет мешать понять удобство этого инструмента, но этому просто нужно отдавать отчёт :)

Слияние бранчей в случае с Git тоже гораздо более лёгкое, в сравнении с TFS. Причина этому кроется в том, как устроены эти системы “изнутри”, но факт остаётся фактом: слияние двух веток в TFS процесс в основном болезненный и сложный, тогда как слияние двух веток в Git процесс совершенно безболезненный и простой. И это мы ещё не начали говорить о rebase и прочих полезных фичах.
При этом в случае с Git отсутствует непонятное ограничение TFS на предмет того, какие бранчи можно сливать: TFS позволяет слияние только непосредстенно “родительского” бранча с “дочерним”, Git же подобных глупых ограничений не имеет и продолжает радовать простотой и безболезненностью процесса.

Я знаю, что из командной строки TFS можно заставить слить два бранча, не связанных непосредстенным наследованием, но скорее всего слияние в этом случае будет ещё более болезненным, чем это можно себе представить. К тому же TFS будет скатываться в режим baseless merge, а тут уж, как говорится, $YOUR_GOD в помощь.

В то время как локальные бранчи – это дело каждого отдельного разработчика, статегия релизов, фич, хотфиксов и т.д. – это стратегия компании. В случае с Git быстро установить простой, удобный и понятный цикл разработки и систему очень просто. Плюс для этого существуют удобные инструменты. Для примера рекомендую оригинальную статью “A successful Git branching model” либо её перевод на русский язык и неплохое дополнение к переводу.

Резюмируя: система контроля версий TFS построена на основе “устаревшей” модели, она неудобна во многих аспектах работы и накладывает ряд странных ограничений. В то же время другие системы контроля версий (например Git) подобных недостатков лишены, плюс добавляют множетво полезных возможностей. Так почему же не выбрать Git?
Microsoft тоже понимает это, поэтому добавляет поддержку Git в TFS как может, но на мой прямой вопрос “если я всё равно использую Git, зачем мне подключать его к TFS и использовать через TFS, в чём смысл?”, заданный несколько раз на разных конфренциях и событиях, прямого ответа дано ни разу не было. Ну на самом деле, не могут же они прямо ответить: “Люди хотят использовать Git, а нам нужно продавать TFS, поэтому мы как-то вкорячим Git в TFS и будем продолжать его продавать” :)

Builds

TFS имеет встроенную систему билдов, позволяющую организовать Continuous Integration (CI, “постоянную интеграцию?”). В данном случае мы имеем TFS Server как центральный сервер билд-конфигураций и можем организовать некоторое количество отдельных (виртуальных) машин, называемых билд-агентами (build agents), которым TFS Server будет давать задачи и которые будут непосредственно заниматься тяжёлой работой – компиляцией, сборкой и т.д.
Билд можно сконфигурировать таким образом, чтобы он запускался вручную, или по расписанию, или в случае изменений в системе контроля версий.

Всё это очень удобно, кроме конфигурации и поддержки всего этого дела. Раньше, в первых версиях TFS, конфигурировать билд нужно было посредством написани MSBuild-скрипта. Для тех, кто не сталкивался – задача не из приятных. И, казалось бы, можно ли сделать процесс ещё более неудобным, чем ручное написание XML-файло с довольно размытой спецификацией? Оказалось – можно, и в следующих версиях Microsoft перевёл конфигурации билдов на Windows Workflow Foundation (который, кстати, был фактически убит чуть позже самой MS) и “прикрутил” к билдам соответствующий визуальный редактор WWF. Всё стало громоздко, невнятно и ещё менее удобно.

Но хорошо, редактор – это на любителя. Однако, опять же, про бранчи: создав бранч в TFS придётся настроить отдельный билд для этого бранча. Пусть сделать это уже не так сложно: конфигурацию билда можно хранить внутри бранча и она, как любой код, так же будет частью бранча.
Но всё же это нужно сделать – раз, и всё же нужно для каждого бранча идти в администрирование билдов и создавать отдельный билд, указывая бранч и конфигурацию. Это если у тебя есть права, а если нет – то кто-то у кого есть должен это сделать. А потом ещё и удалить, когда удаляется уже ненужный бранч.
А бранчи будут. Даже если вы берёте пример с фанатов Apple и заявляете “нам бранчи не надо”. Пусть не фичи, пусть не дивелоперские, но будут бранчи релизов, будут бранчи хотфиксов, как без этого.

Давайте посмотрим на альтернативы, скажем, на JetBrains TeamCity. Если вы “дотнетчик”, то вы пользуетесь ReSharper, то вы знаете, что JetBrains делает хорошие инструменты для разработчиков. TeamCity один из них.
Установка сервера прозрачна и безболезненна. Установка билд-агентов автоматизирована: нужно просто пойти и нажать на ссылку. TeamCity автоматически определяет параметры каждого агента (версии ОС, установленных фреймворков и т.д.) и способен автоматически выбирать подходящие агенты: билд проектов, использующих .net framework 4.5 не будет запущен там, где установлен только 3.5. Плюс, конечно, явно указать какая конфигурация может быть запущена на каких агентах.
Настройка конфигурации билда делается очень удобно и визуально: указываем файл solution, версию visual studio (или msbuild, по вкусу), запускать ли юнит-тесты, какие артифакты (результаты) билда нужно сохранить и т.д. Всё очень удобно. Артифакты потом всегда можно скачать или посмотреть прямо из веб-интерфейса, для любой версии.
Имеются дополнительные приятные “фишки”:
- Встроенный патчер assembly.info файлов, позволяющий автоматически задать версию билда.
- Встроенный nuget-сервер – идея выше всяких похвал. Вы теперь можете “развязать” зависимости от “общих” проектов и библиотек: TeamCity будет собирать библиотеку и публиковать её как nuget-пакет. Остальные проекты и solution’ы будут просто получать nuget-версию, это безумно удобно.
- Возможность автоматически ставить метку в систему контроля версий в процессе билда (удобно для релизов и хотфиксов)
- Возможность “перезапустить” любой билд любой версии – автоматически возьмёт ту же самую ревизию из системы контроля версий и будет работать с ней.

Бранчи, конечно бранчи! В случае использования Git, например, одна билд-конфигурация будет работать со всеми бранчами данного репозитория! Это значит, что создавая/удаляя бранч не нужно трогать/��онфигурировать билд-систему, все бранчи автоматически получают CI! 
Естественно, в интерфесе видно какой именно бранч собирается/собрался/упал.

Плюс многое, многое другое. Например, зависимости билдов друг от друга разного рода. Самое простое -  “когда успешно соберётся А начать собирать Б”. Самое распространённое: “когда я запускаю Deploy-билд, взять артифакты (результаты) из последнего успешного билда А и работать с ними (деплоить куда-то)”. Или не “из последнего”, а “из версии, которую я укажу”. И много чего ещё.

Причём всё это очень просто конфигурируется прямо из веб-интерфейса. Причём конфигурируется и расширяется настолько, насколько хватит фантазиии и требований.

Резюмируя: разница в удобстве и возможностях билд-системы TFS и TeamCity огромна. TeamCity даёт гораздо больше возможностей, с которыми работать гораздо более удобно. Я лично не вижу смысла использовать билд систему TFS даже в том случае, если есть желание использовать TFS для всего остального. Естественно, TeamCity поддерживает систему контоля версий TFS (как и десяток других), но тут выше головы не прынгешь, как с теми же бранчами.

Управление задачами

Сразу скажу, что отличной системы управления задачами я пока ещё не встречал. Тут же скажу, что система управления задачами в TFS на мой взгляд ужасна. Здесь всё: и пользовательский интерфейс, и настройка процесса, и миграция на более новые версии процесса, и репортинг, и трекинг… С этой стороны у TFS ужасно всё. А ведь это как раз та часть, которая должна быть максимально удобной для команды разработчиков.
Я даже не буду тратить время на описание всего этого дела ибо любой, кто сталкивался с этой стороной TFS, от разработчика до Самого Большого Начальника скажет, что работать с эти неудобно, а кто пытался это дело автоматизировать/кастомизировать, добавит ещё и ремарку о том, что всё это очень не гибко.

В качестве аналога здесь можно посмотреть на Jira с плагином Greenhopper. Здесь и гораздо большая гибкость, и более приятный пользовательский интерфейс, и более удобная отчётность (как в использовании, так и в настройке).

Технические детали Jira я тоже описывать не буду, лучше опишу о том, как именно мы использовали этот продукт в процессе разработки так сказать “на пике эффективности”.

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

Положения были следующие:
- в Jira находится backlog, т.е. список фич (stories), которые нужно сделать в продукте.
- в начале итерации (каждые 2 недели) команда организовывала митинг, в процессе которого решала, какие именно stories (из Jira) она будет делать в течение итерации. Stories обсуждались, принималось решение о том, как именно (технически) будет делаться каждая story, т.е. для каждой story создавались tasks. Время митинга – 20-30 минут максимум. Часто – гораздо меньше.
- для каждой story заводилась одна жёлтая карточка (сама story) и несколько белых (tasks). Это быстро, прямо в процессе обсуждения, сидит человек с фломастером и пишет на карточках.
- каждая белая карточка (task) оценивалась в человеко-часах. Обычно, если число превышало 2 рабочих дня, task пересматривался: слишком большой, возможно его можно разбить на пару поменьше.
- Tasks не заводились в Jira. Это – детали имплементаци, никому кроме команды не интересные и, когда работа уже сделана, не имеющие никакого смылса. Так зачем же тратить время и засорять Jira?
- Stories, которые команда решила делать в итерации, помечались меткой с номером итерации (опционально).
- в конце итерации Stories, которые были успешно выполнены, помечались как “готовые” в Jira.

Что происходило в процессе итерации:
- Stories, выбранные командой, располагались на доске в порядке приоритета выполнения (stories вертикально, tasks – горизонтально для каждой story).
- Все члены команды одновременно работали над задачами в порядке приоритета. То есть, не Вася работает над S1, Петя над S2, в Коля над S3, как часто бывает, а все вместе работают над S1, потом над S2, потом над S3. Просто брали одну белую карточку за другой из S1, пока она не сделана, потом переходили к S2 и т.д. Это сильно повысило эффективность, как командной работы, так и в плане того факта, что если мы что-то не успеваем, то мы не успеваем сделать S5, а не имеем S1..5 все сделаны на 90%, но ничего не готово. Плюс это сильно повысило эффективность тестирования, ибо пока когда разработчики сделали S1 и переходят к S2 тестеры уже могут начать тестировать S1, найти там баги и повесить их на доску (ниже).
- Баг, найденный в процессе итерации в разрабатываемой фиче, не считается багом. Это не баг, это – незавершённая работа. Тестер решает, что делать: переложить карточку обратно или завести новую. В большинстве случаев просто заводится новая task-карточка и вешается на доску. Разработчики потом, когда берут следующую задачу, видят: о, в S1 появилось что-то недоделанное, это имеет приоритет над S2, следовательно берём и доделываем. Таким образом, баги, найденные в процессе итерации в разрабатываемой фиче не заводятся в Jira. Ибо с истрической точки зрения не важно, что ты там случайно пропустил, но потом тут же нашёл и исправил, пока ты не сдал в продукт готовую функциональность.
- Баги, находящиеся вне контекста итерации (найденные где-то в чём-то, что уже сдано в продукт), являются обычными stories, так же, как и фичи.
- Тестеры – это часть команды. Поэтому задачи тестеров – это такие же задачи внутри итерации, связанные с разрабатываемыми stories. Это такие же карточки на доске, так же проэстимированные. Например “написать автоматический тест для фичи А” и т.д. Причём начать писать тесты, тест-кейсы и т.д. можно ещё до того, как story будет полностью готова, ибо вся команда знает после планирования, как это должно работать и что должно быть сделано. Тестеры так же полноценно работают в процессе итерации, а не находятся в положении, когда им в конце сбросили “нате, тестируйте теперь над чем мы 2 недели возились!”.

Таким образом мы имеем “чистенькую” картинку в Jira, где у нас в порядке приоритета расписаны фичи продукта, а команды эти фичи реализуют в процессе итераций. Идея в том, что если после итерации S1 сдана, то она сдана полностью: разработана и протестирована. Done-Done, что называется.

Резюмируя: вся эта ужасная сложность с воркайтемами в TFS совершенно не нужна и только замедляет процесс работы. Интерфейс TFS не располагает ни к быстрой удобной работе, ни к быстрому созданию воркайтемов, ни к видению полноты картинки. Jira гораздо более удобна в этой связи, но и тут не нужно перебарщивать. Не нужно пихать в Jira (и в TFS) вообще всё, достаточно иметь там приоритизированный список фич продукта и иметь его в достаточно детальном виде, где каждая фича “атомарна”, автономно тестируема и имеет смысл с точки зрения бизнес-видения. Не нужно пихать в Jira/TFS задачи типа “изменить схему БД”, “написать адаптер для API” или “создать юнит-тест для Х”. Это не имеет никакого смысла, лучше использовать доску.
Если у вас нет физической доски, и вы хотите использовать “электронную”, опять же, Jira+Greenhopper делает это гораздо удобнее TFS.
Про “физическую доску”. В конце каждой итерации мы проводим небольшой митинг (10-15 минут максимум) на предмет “что мы сделали хорошо, что плохо и что в следующий раз будем/не будем делать”. С тех пор, как мы перешли на физические доски, практически каждый раз каждый член каждой команды (команды было три) в категории “что хорошо” отмечал, что физическая доска с карточками – это очень хорошо. Нравилось всем. Так что я сильно рекомендую.

Wiki/Документация

TFS предлагает Sharepoint. Здесь, я думаю, можно и закончить сравнение, ибо это одно из худших решений для ведения документации/wiki, которое можно предположить.

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

Если вам нужна wiki для проекта, или вы хотите делать публичную документацию, или что-то в этом роде, то не нужно ставить для этого TFS, чтобы получить SharePoint. Лучше возьмите Confluence.

Интеграция

Одним из сильных моментов TFS считается то, что все эти фичи интегрированы друг в друга “из коробки”.
Давайте посмотрим, что это означает и как дело обстоит с интеграцией в случае использования сторонних продуктов.

Система контроля версий. TFS интегрирован в Visual Studio, можно делать checkin/checkout и другие операции прямо из студии. Однако Microsoft, решив поддерживать Git, точно так же сделал точно такую же интеграцию Git в Visual Studio. Визуально – всё то же самое. Для тех, кому нравится как VS работает с контролем версий TFS – всё будет точно так же. Однако, в случае с Git, есть и другие варианты: начиная от командной строки до сторонних плагинов к Visual Studio. Лично я довольно много пользуюсь командной строкой, а в качестве интеграции со студией предпочитаю сторонний плагин, так как он даёт мне больше опций. Ваш выбор.

Билд. TFS билды, поддерживают только TFS (может что-то изменилось в последнее время?). TeamCity из коробки поддерживает десяток разных систем, включая и TFS, и Git. Причём поддерживает лучше и удобнее (бранчи, история, интерфейс, настройка, я писал выше). TeamCity интегрируется в VisualStudio (хотя я не понимаю, зачем это надо).
TeamCity к тому же имеет отличные возможности интеграции чего угодно (действительно чего угодно: всё, что мы можем захотеть сделать в процессе билда может очень просто отрапортовать назад в TeamCity, где мы можем сделать фактически что угодно с результатом).
TeamCity при этом уже интегрирована с инструментами типа анализ покрытия тестами, fxcop и т.д.

Управление задачами. А здесь как раз то место, когда интеграция “из коробки” в TFS проигрывает интеграции сторонних компонентов. Например, для того, чтобы ассоциировать изменение с задачей в TFS нужно найти задачу в списке и пометить галочкой. В случае интеграции “сторонних компонентов” достаточно просто указать номер задачи в комментарии к изменению – и всё будет ассоциировано автоматически.

Резюмируя: никаких не то, чтобы проблем, а и вообще сложностей в интеграции “сторонних компонентов” нет. Плюс интеграция эта – расширяема. Например, следующим шагом мы можем (и захотим) включить в нашу экосистему Crucible - отличный продукт для code review, и всё точно так же замечательно будет интегрировано. И т.д. и т.п.

Заключение

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

Я не вижу смысла сейчас пользоваться TFS. И дело даже не в цене TFS по отношению к бесплатности “сторонних компонентов”, дело в том, что каждый из этих компонентов является “специалистом” в своей области, открывающим новые возможности и снимающим имеющиеся ограничения, в то время, как в TFS каждый из этих компонентов лимитирован, негибок и весьма посредственен. С TFS сложно что-то менять, сложно что-то развивать. TFS предписывает определённый шаблон, которому приходится следовать, и этот шаблон предписывает далеко не самый эффективный способ решать задачи.

6/2/2012 8:50:11 AM

Две недели на машине по Великобритании удались как нельзя лучше.


View Larger Map

К сожалению, я забыл включить логгинг на GPS в начале пути и сделал это уже в Южном Уэльсе. Поэтому розовая линия маршрута начинается оттуда и идёт “по часовой стрелке”. А ведь до этого тоже что-то было, например тот же Bath… :)

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

  1. Машину брали на прокат в Avis (на 13 дней). В принципе, можно брать и в Hertz, и в Sixt, но у Avis как раз подвернулись весенние скидки, что определило выбор. В принципе, какие-то скидки у кого-то есть всегда, нужно только найти ;)
  2. Проблем с WiFi не было никаких. Он есть бесплатный практически везде в пабах-барах-кафешках-ресторанах. Есть несколько бесплатных сетей, работающих по территории Великобритании. Там только нужно бесплатно зарегистрироваться.
    Например, “Moto” часто можно встретить на заправках, где есть еда-кофе вроде Burger King, Costa и т.д.
    ”The Cloud” можно встретить где-нибудь в городах в публичных местах.
    Ни один B&B или отель не предлагал интернет за деньги, всегда был бесплатный (хоть и не всегда хорошего качества).
  3. Там, где нет WiFi очень пригодилась WiFi-точка с 3G-симкой. Мы взяли две симки оператора O2, так как нам сказали, что у них самое лучшее покрытие. Лена пользовалась своим iPhone, а я свою симку поставил в WiFi-точку, чтобы иметь доступ к интернет и с таблета и с телефона (который в виду отсутствия симки превратился просто в карманный компьютер).
    3G в Британии есть не везде. Частенько имелся только Edge (E на индикаторе) или даже GPRS (или что-то такое же медленное, кружочек на индикаторе). Была даже более странная ситуация – в каком-то городке телефон не видел сети вообще никакой. Причём выезжаешь “в поле”, где только бараны и фермеры – и вот вам нормальный 3G. Въезжаешь обратно – опять ничего.
    Быть может у них там какой-то конфликт с O2, а симок других операторов у нас не было :) Возможно нужно было взять одну от Vodafone, но нам, честно говоря, хватало.
    Note: при покупке симки для 3G в Британии вам везде будут говорить, что с WiFi-точкой она работать не будет, а будет только с телефоном. А такую, чтобы работала, можно купить только с “родной” WiFi-точкой, которая стоит £20. Но это неправда. Всё отлично работает :)
  4. Отелей никаких нигде заранее не бронировали. План был такой: по ходу решаем, куда едем дальше. И он отлично себя оправдал!
    К вечеру приезжали на место и уже там определялись где переночевать. Преимущество отдавали Bed & Breakfast, в отдельных случаях брали отели. Bed & Breakfast это обычно частные дома, в которых хозяева сдают комнату и с утра готовят завтрак. Знаете, это когда дети выросли и разъехались, а комнаты остались :)
    По качеству это гораздо лучше и “душевнее” среднего отеля, а по цене обычно дешевле. В среднем B&B обходились нам в районе £60-£70 за ночь. Получается, что приличный B&B в Британии дешевле плохонького отельчика без завтрака в Австралии :)
    Процедура поиска комнаты простая: приезжаешь на место, открываешь Booking.Com, Kayak и TripAdvisor (есть приложения для Android, iPhone, можно и браузер использовать) и смотришь, что есть вокруг. Выбор обычно хороший. Ну и просто проехать по улицам – таблички B&B встречаются очень часто, очень распространено это там. Впрочем, почему нет, если подумать? Почти пассивный доход.
    Note: Иногда бывает, что цены, указанные на booking.com и т.д. не совпадают с теми, которые говорят на месте. Иногда они предлагают цену даже дешевле (видимо потому, что уже день/вечер и лучше сдать, чем не сдать), а иногда – дороже. В последнем случае нужно только ткнуть пальцем в booking.com и цена сразу встаёт на место :)
  5. Бесплатный паркинг в мелких городах – не проблема. Но в крупных и людных (Эдинбург, Йорк, Кэмбридж, Оксфорд) это проблема большая. В таких городах предлагается использовать Park & Ride – бесплатные парковки на окраине города + супер дешёвые автобусы до центра. Доезжаешь, бросаешь машину и едешь гулять. Но это не всегда удобно, так как автобусы перестают ходить часам уже к 7 (а по выходным и раньше),  а парковки не всегда предполагают стоянку ночью.
    Это и есть те “отдельные случаи”, когда мы брали отели. Мы искали недорогой отель в центре города со своей бесплатной парковкой. Обычно находили пару-тройку с пометкой “осталась всего одна комната!”. Пометка, кстати, честная – я проверял :)
    Так в Эдинбурге мы останавливались в самом центре города, в Йорке – “внутри стен” в 5 минутах пешком от исторического центра. Машина там не нужна, в городе, поэтому она просто стоит на парковке, зато всегда есть доступ к своим вещам и уезжать проще.
    Note: Если нашёлся подходящий отель с паркингом, лучше позвонить туда и спросить, действительно ли он есть и есть ли на паркинге места. Обычно паркинг не закрепляется за конкретным номером, поэтому кто успел – тот успел. Ну и заодно попросить попридержать оставшийся номер так как “я вот уже тут, в городе, и уже ч��рез 15 минут буду у вас”.
  6. Погода в Британии сами знаете, не ахти: тучи и дожди. Поэтому погоду лучше брать свою! Мы взяли чуток из Сиднея и возили с собой. Поэтому все две с лишним недели над нами на чистом небе светило яркое солнце, температура была 24-26 градусов по всему маршруту. А среди местных, как говорится, “На небе только и разговоров, что о море” :) Везде в пабах, на улицах, на рыбалке (да, мы и с рыбаками пообщались) народ обсуждал, насколько замечательные погоды нынче стоят и как это необычно.
    И вокруг много-много-много красных людей. Потом им будет плохо. Но это ведь потом ;)
    А ведь это всего-навсего мы приехали покататься :) Вот мы уехали – и теперь там опять холодно и дожди.

Вот так. Следите за анонсами Winking smile

4/3/2012 11:09:21 AM

Disclaimer: Внутренних паспортов в Австралии нет. И если не собираешься ехать куда-то за границу, то он и не нужен. Паспорт нужен только для заграничных поездок. Поэтому, чтобы тем, кто читает мою писанину из России было понятнее, я использую термин “загранпаспорт”. Иначе люди путаются. Но здесь это называется просто Passport, и в разговоре с “местными” я тоже говорю “паспорт”. Поэтому тем, кто читает мою писанину из Австралии я пишу этот disclaimer :) 
Вообще-то и в России ведь тоже на загранпаспорте написано “Паспорт”, а не “Заграничный Паспорт”.

Процедура получения загранпаспорта здесь отличается от российской. В России ведь как было: надо пойти в ОВИР, взять форму, заполнить, взять справку с работы, снова придти в ОВИР, подать документы, через месяц снова придти в ОВИР и забрать паспорт.

В последний раз когда я получал загранпаспорт в России форму можно было скачать, но заполнить правильно было невозможно (во всяком случае в условиях костромской реальности): будут придираться и посылать до тех пор, пока ты не пойдёшь в указанный “офис” (на самом деле подвал) на другом конце города, и не заплатишь 150 рублей за то, что странный дядька двумя пальцами заполнит эту форму за тебя. Вот тогда все ошибки моментально перестают быть существенными, их оказывается можно исправить прямо в кабинете в программе, которая сканирует бланк, или же прямо на бумаге ручкой. А без дядьки – никак нельзя, что ты.

Самое неудобное во всей этой процедуре – огромные очереди как на “подать документы”, так и на “получить паспорт”.
Я надеюсь, что сейчас процедура упростилась и дядьки исчезли, надо спросить у родителей, они совсем недавно проходили через это :)

В Австралии тоже нужно заполнять форму, но делается это прямо на правительственном сайте. В форме не нужно перечислять места работы/службы/отсидки за последние 10 лет, фактически она содержит только базовую информацию: имя, место рождения, адрес, номер прав… После заполнения можно просто скачать PDF с уже заполненой формой с сайта и поставить в нём свою подпись.
Кроме этого нужно ещё найти человека, который может подтвердить, что ты – это ты. Требования к такому человеку не такие драконовские, как при подаче заявления на получение гражданства, это может быть любой гражданин, у которого, например, уже есть загранпаспорт, действительный как минимум ещё два года.

В форме заявления этот человек должен указать, что он знает “подателя сего” лично уже более года. Так же он должен подписать с обратной стороны фотографии текстом: “Это на самом деле фотография Василия Пупкина” и поставить роспись.

Далее интересное: с заполненой формой, подписанными фотографиями, правами, свидетельством о гражданстве, свидетельством о рождении, переводом свитедельства о рождении, карточкой медицинского страхования, кредитной карточкой, российским загранпаспортом и всем-всем-всем нужно идти на почту. Подача заявления на получение загранпаспорта делается прямо на почте, по почте же в течение 12 рабочих дней должен придти и паспорт.

На почте нужно играть в игру “убеди китайца, что всё в порядке”. Задачки бывают разных уровней сложности, от “эти две страницы напечатаны разными чернилами – непорядок” (правильный ответ “и чо?”) и до “оригинальное свидетельство о рождении маленькое, зелёное и книжечкой, а перевод – А4, нужно переделать, чтобы было одинаково!” (правильный ответ – попытаться доказать, что свидетельство о рождении вообще не нужно и его нет в списках требований, а если вдруг хочется, то бытие не определяет сознание главное – это содержание, а не форма).
Мне попался квест “слишком большие поля, надо чтобы было меньше, мы, конечно, можем принять, но…”. На этом квесте я проиграл и форму перепечатал.

Фишка, опять же, в том, что на почту ходить легче, чем в ОВИР (и гораздо быстрее, ибо не надо с утра номерки писать на ладошках). А для получения паспорта ходить вообще никуда не надо (я надеюсь) :)

Стоимость австралийского паспорта выше в 7 раз, чем стоимость российского и выдаётся он на срок 10 лет, если я правильно помню. А за “символическую” доплату в $170 можно получить паспорт в течение двух дней вместо двенадцати.

В общем, подготовка к поездке в Великобританию, которая будет первой нашей поездкой в качестве граждан Австралии, идёт.

3/5/2012 11:08:14 AM

В процессе рассмотрения ASP.NET Web Api в связке с Telerik Kendo UI (кстати, весьма рекомендую) из состава полученой мною недавно (бесплатно!) лицензией на Telerik Premium Collection и возникла эта тема.

Из API-метода хотелось бы вернуть некую структуру неизвестного заранее формата.
То есть, например, Kendo Grid ожидает на вход json вида:

[{firstName: Alexey, lastName: Raga, luckyNumber: 28},
{firstName: Mithun, lastName: Chackraborty, luckyNumber: 12}

]

Для одного грида может быть такая вот структура, для другого – другая, настраивает всё это сам пользователь (что где и как показать).
Ну и вообще иногда приходится вернуть что-то, структура чего неизвестна на этапе разработки.

”Собирать” форматированное значение вр��чную тоже, в общем-то, не хочется.
Кроме того, в Web Api реализован автоматический Content Negotiation, что означает, что данные будут возвращаться в том формате, в котором их хочет получить клиент: Xml, Json, OData, может что-то ещё… В этом свете форматировать строки руками вообще выглядит крайне глупо.

В результате раздумий и экспериментов получился такой вот простой вариант решения проблемы: создать “динамический” класс-контейнер, который будет уметь сериализовываться во что там клиент с сервером договорятся:

[Serializable]
public sealed class RowContainer : DynamicObject, ISerializable
{
    private readonly IDictionary<string, object> _values 
        = new Dictionary<string, object>();

    public object this[string memberName]
    {
        set { _values[memberName] = value; }
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
       _values[binder.Name] = value;
        return true;
    }

    void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
    {
        foreach (var value in _values)
            info.AddValue(value.Key, value.Value);
    }
}

Фишка тут в том, что объект хранит все свои значения во внутренней таблице, а при запросе на сериализацию выдаёт их в виде пар ключ-значение.

Наследование от DynamicObject позволяет использовать полученый тип как динамический:

var list = new List<RowContainer>();

dynamic row = new RowContainer();
row.Name = "Alexey";
row.LastName = "Raga";
row.LuckyNumber = 28;
list.Add(row);

row = new RowContainer();
row.Name = "Mithun";
row.LastName = "Chakraborty";
row.LuckyNumber = 12;
list.Add(row);

А реализация индексатора позволяет, в случае необходимости, добавлять значения полей как в коллекцию:

row = new RowContainer();
row["FirstName"] = "Bruce";
row["LastName"] = "Willis";
row["LuckyNumber"] = 666;
list.Add(row);

Теперь можно просто вернуть это дело из метода Web Api и пусть Content Negotiator там сам разбирается как и в каком формате его передать клиенту:

// GET /api/<controller>/id
public IList<RowContainer> Get(int id)
{
    var list = new List<RowContainer>();

    //fill in the list somehow

    return list;
}

P.S. Понятное дело, что можно и тип поприличнее назвать, и TrySetValue реализовать, и, может быть TryGetIndex/TrySetIndex (хотя обычный индексер отлично справляется), но это всё мелочи :)

Powered by BlogEngine.NET 2.5.0.6

About the author

Alexey Raga Alexey Raga
.NET software developer.

E-mail me Send mail

Twitter

Widget Twitter not found.

Root element is missing.X


Archive

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2014

Sign in