________fear_of_underscores

вторник, 25 сентября 2012 г.

F#un tee pipe

В Linux есть прекрасная команда tee, которая берёт вывод какой-то команды, записывает его в файл, и в то же время выводит на консоль:

А ещё в Linux shell есть pipe-lining, очень похожий на pipe-lining из F#.

Эти два обстоятельства и одна задачка, возникшая на работе, натолкнули меня на мысль, что в    F# можно добавить аналог tee. Вот, например, такой код:
open System

let rnd = new Random()
[| for _ in 0..100 -> rnd.Next(100) |] .|> "array.txt" |> Array.sum |> printfn "%i"
создаст массив из случайных целых чисел, запишет его в файл "array.txt", посчитает сумму элементов массива и напечатает её на консоль. Осталось только реализовать этот магический оператор (.|>)!

К счастью, в F# подобные задачки решаются тривиально:
open System.IO

let inline ( .|> ) data fileName =
    File.WriteAllLines(fileName, Seq.map (sprintf "%A") data)
    data

//val inline ( .|> ) : 'a -> string -> 'a when 'a :> seq<'b>
Обратите внимание: сия магия работает только с коллекциями (для меня это самый частый use case). На самом деле, можно использовать WriteAllText и sprintf "%A" data соответственно, но тогда слишком длинные коллекции будут обрубаться.

четверг, 13 сентября 2012 г.

Извлечение ключевых слов текста с помощью TF-IDF метрики

Что такое TF-IDF рассказано в этих прекрасных видео с Coursera (Web Intelligence and Big Data):
TF-IDF
TF-IDF Example

Я же покажу реализацию извлечения ключевых слов с помощью этой метрики на F#:
#if INTERACTIVE
#r @"..\packages\HtmlAgilityPack.1.4.6\lib\Net45\HtmlAgilityPack.dll"
#endif

open System
open System.Collections.Generic
open System.Collections.Concurrent
open System.Linq
open System.Text
open System.Text.RegularExpressions
open System.IO
open System.Net
open Microsoft.FSharp.Control.WebExtensions
open HtmlAgilityPack

let makeTFMap (text : string array) =
    let dict = new Dictionary<_, _>()
    for word in text do
        if dict.ContainsKey word then
            dict.[word] <- dict.[word] + 1
        else
            dict.Add(word, 1)
    dict

let IDF (uniqueTokens : string array) =
    let logWebSizeEstimate = Math.Log(5e10, 2.)
    let dict = new ConcurrentDictionary<_, _>()
    seq {
        for word in uniqueTokens ->
            async {
                let req = WebRequest.Create(@"http://www.google.com/search?q=" + word) :?> HttpWebRequest
                req.Timeout <- 5000
                req.UserAgent <-
                    "Mozilla/4.1 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)"
                let! resp = req.AsyncGetResponse()
                use stream = (resp :?> HttpWebResponse).GetResponseStream()
                use reader = new StreamReader(stream)
                let! html = reader.AsyncReadToEnd()
                let doc = new HtmlDocument()
                doc.LoadHtml html
                let stats = doc.DocumentNode.SelectSingleNode(@"//div[@id = 'resultStats']").InnerText
                let numStr = Regex.Match(stats, @"[0-9,]+").Value.Replace(",", "")
                let idf = logWebSizeEstimate - Math.Log(float numStr, 2.)
                dict.AddOrUpdate(word, idf, (fun _ _ -> 0.)) |> ignore
            }
    }
    |> Async.Parallel
    |> Async.RunSynchronously |> ignore
    dict

let TF_IDF (tf : Dictionary<_, _>) (idf : ConcurrentDictionary<_, _>) =
    let uniqueTokens = tf.Keys.ToArray()
    dict [ for word in uniqueTokens ->  word, float tf.[word] * idf.[word] ]

let analyze (text : string) =
    let tokens = text.Split([|' '; '\n'; '\t'; '.'|], StringSplitOptions.RemoveEmptyEntries)
    let tf = makeTFMap tokens
    TF_IDF tf (IDF <| tf.Keys.ToArray())

let printTop text num =
    let results = analyze text
    let hot10 =
        Array.sortBy (fun (kvp : KeyValuePair<_, _>) -> kvp.Value) (results.ToArray())
        |> Array.rev |> Seq.take num
    for kvp in hot10 do
        printfn "%s has TF-IDF of %f" kvp.Key kvp.Value
Пример использования:
let article = 
    "Акции протеста против антиисламского фильма распространяются по всему миру: демонстрации прошли в Ливии,
     Йемене, Египте, Ираке, Тунисе и других странах. Власти Афганистана и Пакистана закрыли доступ к YouTube,
     дабы попытаться не допустить просмотра скандального фильма. Участники акций протеста считают, что
     размещенный в интернете неким 52-летним американцем из Калифорнии любительский фильм, оскорбляет пророка
     Мухаммеда. При штурме американского посольства в Ливии во время акции протеста 
     во вторник погиб американский посол."
printTop article 10
(Текст взят из этой статьи BBC)
Результат:
протеста has TF-IDF of 30.083060
антиисламского has TF-IDF of 18.183107
штурме has TF-IDF of 15.472137
американцем has TF-IDF of 14.996109
Йемене, has TF-IDF of 14.777763
оскорбляет has TF-IDF of 14.299300
скандального has TF-IDF of 14.253497
Тунисе has TF-IDF of 14.058740
размещенный has TF-IDF of 13.757642
неким has TF-IDF of 13.116506
Real: 00:00:09.695, CPU: 00:00:01.248, GC gen0: 41, gen1: 16, gen2: 1

среда, 12 сентября 2012 г.

F#un code generation

На работе (Erlang/C#-разработка) подозрительно часто приходится писать небольшие скрипты для кодогенерации. Пишу я их, конечно, на F# — и он успел зарекомендовать себя как отличный инструмент для этого (кто бы сомневался!). Сейчас же я немного покодогенерировал не на работе, а делая новую версию моего шаблона проекта DataAnalysisApp.

Задача: сделать кодогенерацию директив компилятора F# для подключения .dll-библиотек из пакетов NuGet'a.

Эти библиотеки расположены в подпапках \lib директории с пакетами, а иногда — в подпапках этих подпапок с названиями вида \Net45 или \40.

Решение:
let printInterpreterRDirectives dir =
    let formDirective path =
        sprintf """#r @"..%s" """ <| Regex.Match(path, @"(?i:\\packages\\.*\\lib\\.*)").Value
    
    Directory.EnumerateDirectories dir
    |> Seq.map (Directory.EnumerateDirectories >> Seq.find (fun y -> Regex.IsMatch(y, @"(?i:\\lib$)")))
    |> Seq.collect (fun dir -> 
                        seq {
                            for subdir in Directory.EnumerateDirectories dir do
                                if Regex.IsMatch(subdir, @"(?i:\\(net)?4(0|5)$)") then 
                                    yield subdir
                            yield dir
                        })
    |> Seq.collect (Directory.EnumerateFiles >> Seq.filter (fun file -> FileInfo(file).Extension = ".dll"))
    |> Seq.map formDirective
    |> Seq.iter (printfn "%s")

printInterpreterRDirectives @"C:\...\Visual Studio 2012\Projects\Data Analysis App 2012\packages"
Получается портянка из вот такого:

#r @"..\packages\FSPowerPack.Community.2.1.3.1\Lib\Net40\FSharp.PowerPack.Parallel.Seq.dll"
#r @"..\packages\HtmlAgilityPack.1.4.6\lib\Net45\HtmlAgilityPack.dll"
#r @"..\packages\MathNet.Numerics.2.2.1\lib\Net40\MathNet.Numerics.dll"
...


Довольно красивое сочетание pipe-lining'a, композиции и вычислительного выражения. Ну и регэкспы, конечно!

Легко заметить, что в решении используется одна из новых возможностей F#: triple-quoted strings. Кстати, скоро будет пост с анализом этих самых новых возможностей))
Ну и да — на самом деле можно сделать проще, воспользовавшись методом EnumerateDirectories с другой арностью:
let printInterpreterRDirectives' dir =
    let formDirective path =
        sprintf """#r @"..%s" """ <| Regex.Match(path, @"(?i:\\packages\\.*\\lib\\.*)").Value
    let enumSubdirs pattern = Directory.EnumerateDirectories(dir, pattern, SearchOption.AllDirectories)
    
    List.fold (fun acc pattern -> Seq.append acc <| enumSubdirs pattern) Seq.empty
              ["lib"; "net40"; "net45"; "40"; "45"]
    |> Seq.collect (Directory.EnumerateFiles >> Seq.filter (fun file -> FileInfo(file).Extension = ".dll"))
    |> Seq.map formDirective
    |> Seq.iter (printfn "%s")

... но эта версия не показывает всю няшность того, насколько просто записать сложные сценарии с помощью функциональных возможностей F#))

пятница, 7 сентября 2012 г.

Простая задачка

Given that Pi can be estimated using the function 4 * (1 – 1/3 + 1/5 – 1/7 + …) with more terms giving greater accuracy, write a function that calculates Pi to an accuracy of 5 decimal places.
Моё решение
let pi precision =
    let rec helper pre curr digit sign =
        if Math.Abs(pre * 4. - curr * 4.) < precision then
            4. * curr
        else
            helper curr ((if sign then (+) else (-)) curr 
                   (1. / (digit + 2.))) (digit + 2.) (not sign)
    helper Double.PositiveInfinity 1. 1. false
pi 0.00001 |> printfn "Estimated Pi: %f"
Сможете решить короче? ;)

среда, 5 сентября 2012 г.

F#un for Kids

Функция — это штука, которой мы даём что-то, она что-то с этим делает, и что-то отдаёт нам обратно.

Тип функции — это описание того, что ей нужно дать и что она вернёт.

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