F # in practice

    Introduction


    Perhaps the two most frequently asked (hence, burning) questions in the comments to my review article about F # were as follows:
    1. Why is it so similar to OCaml?
    2. What the hell did he give up?
    The answer to the first question is not particularly difficult - it is so similar to OCaml, because it is made entirely on its basis. Is it good or bad? Yes, rather good, this is clearly better than coming up with a completely new syntax that is not yet known how good it will be. Plus, there is quite a lot of documentation on OCaml, so even at first there should be no problems with (self) learning.
    The second question is much more difficult to understand, especially now when the language is in a beta state and is so far only an object of study for overly curious programmers. However, despite a rather brief acquaintance with him, I already had occasion to use it once to achieve completely pragmatic goals, which I will tell you about in this short post.
    I will make a reservation in advance, of course, not the last thing that prompted me to solve the task in F # - the desire to practice in a new language. Of course, the program could also be written in C #, and perhaps it would have been a little longer (I repeat, perhaps I did not check). One way or another, the program was written, and it did its job.

    Problem


    The small company where I work is engaged in the creation of various information support systems for domestic submarines. The orders are one-time, quite specific, so until now we have never encountered the problems inherent in product development. For example, with localization. However, it so happened that foreign customers of the Russian defense industry quite unexpectedly had a desire to place our system on some export ships. I will not discuss the topic “Should I have foreseen such an opportunity in advance?” Well, let's say it was necessary, but this does not apply to the topic.
    Our application must be said to include more than a hundred diverse forms written in XAML, dedicated to various calculation tasks, information windows, etc., arranged in a dozen projects and subfolders in them. And, horror, the Russian lines were generously scattered in an even layer over them all. (As it turned out a little later, there were about 1000 lines). And something had to be done with this.

    Decision


    First of all, I abandoned the localization technology promoted by Microsoft almost immediately, because on the one hand it is rather complicated (all these satellite assemblies are scattered in folders, the need to assign an id to all components, and a not very clear usage model). On the other hand, its capabilities, mainly the ability to switch the language in real time, were completely useless in this situation, since it was necessary to get only one copy in another language, and Vietnamese sailors are unlikely to urgently need its Russian-language analogue on the ship.
    So in the end, it was decided to make everything a lot simpler - put all the lines in a ResourceDictionary, which then combine with the main dictionary located in App.xaml, and add them as StaticResource in forms. Like this, generally.
    A program in F # that parses all xaml files in search of Russian lines, changes them and also creates a separate file for the dictionary I wrote in less than an hour, it takes less than a hundred lines, along with comments and my passion, every next function in the pipeline, whatever little she was, write in a new line. And she processed all the files for a little over a second. Something about speed I will mention later.
    At first I thought about telling each method in turn, but then I decided to lay out the whole text so that you could read the code yourself for fun, and decide how difficult it is to read the functional code from the sheet. And by the way, contrary to popular belief, that FY are suitable for those who want to think a lot, but write a little, this particular program didn’t make me think much. Everything happened, as our Western brothers say straightforward, that is, in the forehead.
    In general, here is the code:
    #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


    I think it is worthwhile to pay attention to two functions that use the technology of list initialization - for xml nodes and file names, an example of one of which I just cited in a review article. Also of interest, I think, is the LocalizeNode function, which returns the so-called option value. This is an analogue of the nullable type, which has two variants Some (value) if some value is returned, and None if there is no value. This type is used in the List.concat function, which is similar to List.map, except that it accepts a mapping function that returns an option type (string * string option in this case) and adds only Some values ​​to the final list. Essentially automatically adds List.filter to List.map ( fun i -> i <> None).
    In addition, note that in the main function localizeDirectory, the processing of all files is parallelized to all available kernels on the computer, which allows you to 100% load the computer and obviously reduce the operating time. For this, only three gestures and no ThreadPools are enough, not to mention monitors with other semaphores.
    On the other hand, the program is interesting (and just specific to F #) because it actively uses the CLR, in this case XmlDocument, XmlNode, and other classes from System.Xml. It is in this that I see at the moment the main advantage of it over other functional languages.
    Well, that's basically it. I understand that it’s not good news that of course, but maybe with this straightforward example, someone can draw a conclusion for themselves about the prospects or lack thereof of F #.

    Also popular now: