Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

C# Zen Coding уже на F#

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

В моем предыдущем посте, где я описал идею CSharpZen, я пообещал две вещи – дописать расширение для Visual Studio 2010, а также записать вебкаст того, как это можно использовать. Но пока я дописывал код для трансформатора, мне снова показалось что код может стать более понятным если его переписать на F#. И понеслось…

Почему опять F#? Я уже писал о том что строковые трансформации лучше делать на F# и что они хоть и работают медленнее (хотя Zen Coding вообще написан на питоне и использует DLR), но код легче понимать. А у меня еще появляется работа с массивами неопределенной длины (например при обработке свойств может быть p:sb x а может p:new sb x) что весьма неплохо ложится на pattern matching.

В этом посте я хочу пройтись по проблемам прошлого поста и показать как в очередной раз F# позволяет нам производить аналогичный код но в более понятной форме.

Проблема использования метаданных

Помните как мы определили метаданные для видимости как аттрибуты? Так вот, этот подход хоть и казался полезным (держим метаданные рядом с полями), но на самом деле был жутко неудобным. В F# такой подход не сработает. Вместо этого, мы определяем перечисление отдельно…

type Visibility =
| Public
| Internal
| ProtectedInternal
| Protected
| Private

…а метаданные определяем как массив кортежей которые содержат все нужные нам данные.

type Constants =
  static member visibilityTokens =
    [|(Visibility.Public, "+", "public"); 
      (Visibility.Internal, @"\", "internal");
      (Visibility.ProtectedInternal, @"/\", "protected internal");
      (Visibility.Protected, @"/", "protected");
      (Visibility.Private, "-", "private")|]
  ...

Теперь для поиска конретных элементов метаданных мы просто используем Array.find по кортежам. Например, та же функция вытаскивания Visibility из начала строки теперь выглядит вот так:

static member getVisibilityToken (entry:String) =
  try
    let idx = Constants.visibilityTokens |> Array.findIndex(fun (a,b,c) -> entry.StartsWith(b))
    let (a, b, c) = Constants.visibilityTokens.[idx]
    (a, entry.Replace(b, String.Empty))
  with
    :? KeyNotFoundException ->
       let (a, b, c) = Constants.visibilityTokens.[0]
       (a, entry)

Свойства расширения

F# знаменит тем что можно делать не только extension methods но и extension properties. Это хорошо. Единственная проблема это то, что методы расширения приходится класть в отдельный модуль, а я это не люблю – предпочитаю держать все в одном файле и типе, безо всяких там модулей. А тут не получилось – пришлось описывать все отдельно:

module Extensions =
  type String with
    member x.Cap =
      let parts = x.Split([|'_'|], StringSplitOptions.RemoveEmptyEntries)
      parts |> Array.map(fun f -> Char.ToUpper(f.[0]).ToString() +
               (if f.Length > 1 then f.Substring(1) else String.Empty))
            |> String.Concat

Этот код вообще эклектичен – взять хотя бы «заинлайненый» блок if-then-else прямо внутри map-а. Это что-то. И это что-то не очень-то читабельно. Хотя кому как.

Сборщик кода

Помните CodeBuilder? Так вот, решил я его на F# переписать. В чем-то он лучше, но вот навязчивое желание F# сохранять результаты вычислений или их игнорировать весьма раздражает. Код ниже дает качественно новое определение фразе «послать в игнор»:

type CodeBuilder(spaces) =
  let mutable indent = 0
  let sb = StringBuilder()
  member x.IndentText =
    String.Empty.PadRight(spaces * indent)
  member x.Indent() =
    indent <- indent + 1
    x
  member x.Unindent() =
    indent <- Math.Max(0, indent - 1)
    x
  member x.Append(text:String) =
    sb.Append(x.IndentText).Append(text) |> ignore
    x
  member x.AppendLine(text:String) =
    sb.Append(x.IndentText).AppendLine(text) |> ignore
    x
  override x.ToString() =
    sb.ToString()

Неадекват с мутабельностью и null

Кажется я уже об этом как-то писал. Суть в том, что F# лютой ненавистью ненавидит мутабельность и null. То есть он их поддерживает, но чтобы их использовать… короче Жигули тоже поддерживают езду по дороге, скажем так. Но то, что я вам сейчас покажу это вообще лебединая песня.

Для начала напомню что для того чтобы значение можно было изменять, его нужно пометить как mutable и для записи использовать оператор <-:

let mutable x = 0
x <- 2

Все бы хорошо, но это нелегитивно внутри паттерн-мэтчинга:

match myDrug with
| Vicodin -> x <- 2 // Вам нельзя.

Суть в том, что при таком раскладе вы больше не можете пользоваться «обычным» подходом, вам нужно пересесть на синтаксис который использует ‘ref cells’. Я даже это переводить не буду, почитать можно тут. Если коротко, то используется совсем другой синтаксис:

let x = ref 0
x := !x + 1

А теперь представьте что вам нужно делать то же самое но для nullable-полей. Помните мои меременные context и rootContext? Вот-вот. Они nullable, и как вы помните, F# не поддерживает эту парадигму (точнее поддерживает, но через одно место) в результате чего мне не написать подобное:

let context:IVisitable = null     // fail
let context:IVisitable = ref null // epic fail

На самом деле, F# имеет свою парадигму option<'t> для подобных вещей. Только она тоже сработает только после дозы шаманства:

let context : IVisitable option = ref none // FAIL
let context : Ref<IVisitable option> = ref None // OK, finally *phew*

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

Мутабельность переменных

И еще про мутабельность. В свое время я написал статейку про то как я использую F# DSL для оценки проектов. Так вот, там фигурирует такая неопрятная вещь как публичное поле-список:

[<DefaultValue>] val mutable Groups : Group list

Для отказа от List<T> реальных причин нет – зато есть желание делать все чуть более «функционально». За это мы платим тем, что при добавлении элемента, список полностью заменяется. Не знаю стоимость этой операции, возможно не так дорого, но выглядит странно.

x.Groups <- newGroup :: x.Groups

Зато теперь я могу показать как выглядит определение пространства имен:

type Namespace() =
  [<DefaultValue>] val mutable Name : String
  [<DefaultValue>] val mutable Classes : Class list
  ...

Вот вам и ужасный синтаксис во всей «красе». Зато все работает. Главные плюсы вообщем-то не здесь, а в паттерн-матчинге самого парсера.

Паттерн матчинг рулит

Вот собственно где все самое интересное. То, как организован паттер-матчинг в F# позволяет нам лаконично (ненавижу это слово!) описывать разные условия. Вот небольшой пример:

// split the new classifier and match against it
let parts = x.splitToList(newClassifier, ':')
match parts with
| (* NAMESPACE *) "ns" :: tail ->
  let ns = Namespace()
  match tail with
  | name :: _ -> ns.Name <- name
  | _ -> ns.Name <- co.DefaultNamespace
  let nsv = ns :> IVisitable
  context := Some nsv
  setRootContext nsv

Получив классификатор, мы можем проверить, есть у него имя или нет. При этом избыточные элементы мэтчинга отбрасываются. Но если нам вдруг понадобится иметь «сложные» классификаторы вроде a:b:c то добавить их поддержку проще простого – это всего лишь еще один кейс:

| name :: somethingElse :: -> // что-то умное

Зато гибкость теряется в весьма неудобной (излишней) конверсии Namespace в Visitable. О да, кстати, давайте я быстренько покажу как работают интерфейсы в F#.

Интерфейсы

У нас всего один интерфейс – IVisitable. Но в F# ключевое слово interface используется только когда вы реализуете интерфейс в отдельном классе. Если вы просто хотите объявить интерфейс, то делаете класс с абстрактными объявлениями методов:

type IVisitable = 
  abstract Visit : CodeBuilder * ConversionOptions -> unit

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

type Namespace() =
  [<DefaultValue>] val mutable Name : String
  [<DefaultValue>] val mutable Classes : Class list
  interface IVisitable with
    member x.Visit(cb, co) =
      cb.Append("namespace ").Append(x.Name.Cap).AppendLine(" {").Indent() |> ignore
      List.iter(fun c -> (c :> IVisitable).Visit(cb, co)) |> ignore
      cb.Unindent().AppendLine("}") |> ignore

Многоуровневый паттерн-мэтчинг

В парсере ни много ни мало 5 (пять!!!) уровней паттерн-мэтчинга. Вот четыре их них которые задействованы в разборе определения класса:

| (* CLASS *) "c" :: tail ->
  // get the name
  match tail with
  | [] -> raise(Exception("Class is missing a name"))
  | name :: _ ->
    let c = Class(name, visibility)
    let cv = c :> IVisitable
    context := Some cv
    match !rootContext with
    | Some(x) ->
      match x with
      | :? Namespace as ns -> ns.Classes <- c :: ns.Classes
      | _ -> ()
    | None -> ()
    setRootContext cv

Поскольку класс обязательно нужно положить внутрь простанства имен (если таковое имеется), нам приходится делать двойное сравнение с Option<Namespace> – сначала нужно проверить что это действительно Some(x), а потом проверить что тип совпадает (мало ли…). Проблема в том, что само по себе Ref<Namespace option> не ведет себя полиморфно и одним оператором :? в мэтче не обойтись. А жаль. Было бы удобно.

Реализация свойств

Ну и последний вынос мозга на сегодня. Помните я говорил что в свойство можно вставлять слово new? Интересно, но pattern matching возвращает значения. В контексте свойства мы можем взять свойство args (хвост наших токенов) и вернуть из него кортеж!

let (name, mustInit, skipCount) = match args with
| name :: _ when typeName.IsNotAClassifier -> (name, false, 1)
| "new" :: name :: _ when typeName.IsNotAClassifier -> (name, false, 2)
| _ -> raise(Exception("Property name is missing"))
let p = Property(name, visibility, typeName, mustInit)

Угадайте зачем нужен skipCount. На самом деле он нужен чтобы «отмотать» несколько первых элементов списка args для последующей обработки. Сама отмотка реализована достаточно брутально:

let rec skipElements count theList =
  match theList with
  | h :: t when count > 0 -> skipElements (count-1) t
  | _ -> theList

Думаете что theList должен быть первым параметром? А вот и нет. Если определить его именно так, можно получить частичное применение «огрызка» нашего списка токенов:

args |> skipElements 2 |> buildStructure

Вот собственно зачем нам был нужен skipElements.

Заключение

Интересно почему обработка строк и массивов всегдя тянет меня на F# – ведь в нем море проблем в которых можно утонуть и, что самое главное, когда пишешь код в нефункциональном стиле, написание занимает в 4 раза больше времени чем на C#. Возможно имеет больший смысл писать обработку на кастомном DSL, предназначенном именно для таких задач. А на чем писать такой DSL? Возможно на F#.

Я обновил проект, так что если интересно, весь описанный в этом посте F# код лежит в репозитарии.

Интересное наблюдение: после нескольких часов мучений с F# чувствую себя не героем а идиотом. К чему бы это? ■

Advertisements

Written by Dmitri

5 марта 2010 в 22:45

Опубликовано в C#, f#

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

Subscribe to comments with RSS.

  1. насчет mutable nullable — это конечно жестокий код, но ты точно уверен что это единственный выход? Все это для F# настолько инородное, что мне почему-то кажется, должен быть более functional-style реализовать этот парсинг.А почему бы не передавать визиторы как параметры в buildStructure?.

    Meroving

    27 марта 2010 at 11:01

    • Тут есть варианты. Вообще по-хорошему нужно декларативно описывать все структуры и парсер создавать соответственно. Но я почему-то поленился все это изучить. Впрочем, уже все написано и работает, так что нет смысла горевать :)

      Dmitri

      27 марта 2010 at 13:19


Оставить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: