Pull to refresh

F# на практике

Reading time 8 min
Views 1.9K

Введение


Пожалуй два наиболее часто задаваемых (следовательно, животрепещущих) вопроса в комментариях к моей обзорной статье о 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#.
Tags:
Hubs:
+11
Comments 6
Comments Comments 6

Articles