Введение


Пожалуй два наиболее часто задаваемых (следовательно, животрепещущих) вопроса в комментариях к моей обзорной статье о F# были следующие:
1. Почему он так похож на OCaml?
2. На кой черт он вообще сдался?
Ответ на первый вопрос не представляет особой сложности — он так похож на OCaml, потому что сделан целиком и полностью на его основе. Хорошо это или плохо? Да скорее хорошо, это явно лучше, чем придумывать совершенно новый синтаксис, который еще не известно, насколько будет хорош. Плюс к тому, по OCaml достаточно много документации, так что даже на первых порах проблем с (само)обучением быть не должно.
Со вторым вопросом разобраться куда сложнее, особенно сейчас, когда язык пребывает в состоянии беты и является пока что лишь объектом изучения излишне любознательных программистов. Однако несмотря на довольно краткое с ним знакомство, мне уже довелось разок применить его для достижения вполне прагматических целей, о чем и поведаю в этом небольшом посте.
Заранее оговорюсь, конечно же, не последнее, что побудило меня решать поставленную задачу именно на F# — желание попрактиковаться в новом языке. Конечно же, программу можно было написать и на C#, и возможно она получилась бы ненамного длиннее (повторюсь, возможно, я не проверял). Так или иначе, программа была написана, и дело свое сделала.

Проблема


Небольшая компания, где я работаю, занимается созданием различных систем информационной поддержки для отечественных субмарин. Заказы разовые, вполне конкретные, так что до сего момента у нас никогда не возникало проблем, присущих разработке продуктов. Например, с локализацией. Однако так случилось, что у зарубежных клиентов российской оборонки совершенно неожиданно появилось желание разместить нашу систему на некоторых экспортных кораблях. Я не буду дискутировать на тему «Надо ли было предусмотреть такую возможность заранее?» Ну допустим, надо было, только к теме это не относится.
Приложение наше надо сказать включает в себя более сотни разноплановых форм написанных на XAML, посвященных различным рассчетным задачам, информационным окнам и.т.д., скомпонованным по десятку проектов и вложенных в них папок. И о ужас, русские строки были щедро разбросаны ровным слоем по ним всем. (Как оказалось чуть позже, строк было порядка 1000). И с этим надо было что-то делать.

Решение


Прежде всего, от продвигаемой Microsoft технологии локализации я отказался почти сразу, поскольку с одной стороны она довольно сложна (все эти сборки-сателлиты раскиданные по папкам, необходимость всем компонентам присвоить id, и не особо ясная модель использования). С другой, ее возможности, главным образом возможность переключать язык в реальном времени, в данной ситуации были совершенно бесполезны, поскольку необходимо получить всего одну копию на другом языке, и вьетнамским морякам вряд ли срочно понадобится на корабле ее русскоязычный аналог.
Так что в итоге решено было сделать все намного проще — вынести все строки в ResourceDictionary, который потом объединить с главным словарем, располагающемся в App.xaml, а в формах их прибиндить как StaticResource. Вот так, в общем.
Программу на F#, которая парсит все xaml-файлы в поисках русских строк, меняет их а также создает отдельный файл для словаря я написал менее чем за час, занимает она менее ста строчек вместе с комментариями и моей страстью каждую следующую функцию в трубопроводе, какой бы маленькой она ни была, писать в новой строке. А обрабатывала она все файлы чуть более секунды. Кое-что о быстродействии я упомяну позже.
Я сперва думал поочередно рассказать про каждый метод, но потом решил выложить весь текст целиком, чтобы вы могли ради интереса прочесть код сами, и вынести решение, насколько сложно читать с листа функциональный код. Да и кстати, вопреки расхожему мнению, что ФЯ подходят для тех кто хочет много думать, но мало писать, эта конкретная программа особенно меня задумываться не заставила. Все происходило, как лю��ят говорить наши западные братья straightforward, то бишь в лоб.
В общем, вот такой код:
#light
open System
open System.Xml
open System.IO
open System.Collections

let mutable i = 0 //Аккумулятор для ключа ресурса
 
// Разворачивает дерево всех узлов xml в список, включая аттрибуты
let rec nodes (node:XmlNode) =
    seq { if (node.NodeType <> XmlNodeType.Comment) then yield node
          if (node.Attributes <> null) then
            for attr in node.Attributes do yield attr
          for child in node.ChildNodes do yield! (nodes child)}
            
//Поиск всех XAML файлов во всех поддиректориях текущей директории
let rec xamlFiles dir filter =
    seq { yield! Directory.GetFiles(dir, filter)
          for subdir in Directory.GetDirectories(dir) do yield! xamlFiles subdir filter}
          
// Запись документа в файл
let writeXml (doc:XmlDocument) (file:string) =
    let xtw = new XmlTextWriter(file, null)
    xtw.Formatting <- Formatting.Indented
    doc.WriteContentTo(xtw)
    xtw.Close()

//Проверяет необходимо ли локализовать
let needLocalize (node:XmlNode) =
    let isRussian = Seq.exists (fun ch -> match ch with
                         |'а'..'я'|'А'..'Я' -> true
                         |_ -> false)
    node.Value <> null && isRussian node.Value

//Если узел русский, меняет его имя на шаблон. Имеет тип (string*string) option
let localizeNode (node:XmlNode) =
    if (needLocalize node) then
        let oldValue = node.Value.Trim()
        i <- i+1
        let key = "Title_"+ i.ToString()
        let newValue = sprintf "{StaticResource %s}" key
        match node.NodeType with
        |XmlNodeType.Element -> (node :?> XmlElement).SetAttribute("Content", newValue)
                                node.Value <- null
        |XmlNodeType.Text -> (node.ParentNode :?> XmlElement).SetAttribute("Content", newValue)
                             node.Value <- null
        |_ -> node.Value <- newValue
        Some(key, oldValue)
    else None

//Функция локализации одного файла XAML. Выдает список русских строк в виде(ключ, строка)
let localizeXaml (file:string) =
    let doc = new XmlDocument()
    doc.Load(file)
    let rusDict = nodes doc
                  |> Seq.to_list
                  |> List.choose localizeNode //map, который выбирает только Some элементы
    File.Copy(file,file+".tmp",true)
    writeXml doc file
    rusDict

//Добавляет элемент в словарь
let addResource (doc:XmlDocument) (key, value) =
    let elm = doc.CreateElement("system","String","clr-namespace:System;assembly=mscorlib")
    elm.SetAttribute("Key","http://schemas.microsoft.com/winfx/2006/xaml",key)|>ignore
    elm.AppendChild(doc.CreateTextNode(value))|> ignore
    doc.FirstChild.AppendChild(elm) |> ignore
    
          
//Функция локализации всех XAML файлов в поддиректориях
let localizeDirectory dir =
    let dict = //Создаем словарь, определяем необходимые namespaces
        let tmp = new XmlDocument()
        let fst = tmp.CreateElement("", "ResourceDictionary","http://schemas.microsoft.com/winfx/2006/xaml/presentation")
        fst.SetAttribute("xmlns:system","clr-namespace:System;assembly=mscorlib")
        fst.SetAttribute("xmlns:x","http://schemas.microsoft.com/winfx/2006/xaml")
        tmp.AppendChild(fst) |> ignore
        tmp
    xamlFiles dir "*.xaml"
        |> Seq.to_list
        |> List.filter (fun file -> int (File.GetAttributes(file) &&& FileAttributes.ReadOnly) = 0)
        |> List.map (fun x -> async {return localizeXaml x})
        |> Async.Parallel
        |> Async.Run
        |> Array.to_list
        |> List.iter (fun lst -> List.iter (addResource dict) lst)
    writeXml dict "dict.xml"

//Запускаем программу
localizeDirectory Environment.CurrentDirectory


Думаю нелишне обратить внимание на две функции, использующие технологию инциализации списков — для узлов xml и названий файлов, пример одной из которых я как раз и приводил в обзорной статье. Также интерес думаю вызывает функция LocalizeNode, которая возвращает так называемое option значение. Это аналог nullable типа, который имеет два варианта Some(значение) если какое-то значение выдается, и None, если никакого значения нет. Этот тип используется в функции List.concat, который аналогичен List.map, за тем исключением, что принимает функцию маппирования, возвращающую option тип (string*string option в данном случае), и добавляет в конечный список только Some-значения. По сути автоматически добавляет к List.map List.filter (fun i -> i <> None).
Кроме того, обратите внимание, в главной функции localizeDirectory обработка всех файлов распараллелена на все имеющиеся ядра на компьютере, что позволяет сделать 100% загрузку компьютера и очевидно сократить время работы. Для этого достаточно всего трех телодвижений и никаких ThreadPool'ов, не говоря уж о мониторах с прочими семафорами.
С другой стороны программа интересна (и как раз специфична для F#) тем, что активно использует CLR, в данном случае XmlDocument, XmlNode и прочие классы из System.Xml. Именно в этом я вижу на данный момент основное преимущество его над другими функциональными языками.
Ну вот в общем-то и все. Я понимаю, не бог весть что конечно, но может и на этом незамысловатом примере кто-то сможет сделать для себя вывод и о перспективности или отсутствии оной у F#.