Лекция 3

Деревья


In [24]:
type 't tree  = Nil | Node of 't * 't tree * 't tree

let Leaf x = Node(x, Nil, Nil)

let t = Node(1, Node(2, Nil, Leaf(3)), Leaf(4))

let rec size = function
    | Nil -> 0
    | Node(_, L, R) -> 1 + (size L) + (size R)
    
size t


Out[24]:
4

Непонятно к чему относящийся код


In [26]:
1 |> (+)1 |> (*)2 |> printfn "%d"


4

In [27]:
let plus1 x f = f(x + 1)
let t2 x f = f(x * 2)

plus1 1 (fun x -> t2 x (printfn "%d"))


4

In [28]:
plus1 1 <| t2 <| printfn "%d"


4

Продолжения

Продолжение - такая функция, которая вызывается после завершения нашей функции.


In [29]:
let rec len cont = function
    | [] -> cont(0)
    | h::t -> len (fun x -> cont(x + 1)) t
    
len (fun x -> x) [1..100]


Out[29]:
100

In [34]:
let rec size t cont =
    match t with
    | Nil -> cont 0
    | Node(_, L, R) -> size L (fun sL -> size R (fun sR -> cont(1 + sL + sR)))
    
size t (fun x -> x)


Out[34]:
4

Как видно, оно позволяет такие нелинейные рекурсии делать хвостовыми. Можно еще совместить с аккумулятором.


In [36]:
let rec size t acc cont =
    match t with
    | Nil -> cont acc
    | Node(_, L, R) -> size L (1 + acc) (fun sL -> size R sL cont)
    
size t 0 (fun x -> x)


Out[36]:
4

Продолжение используются в реальной жизни, например, для асинхронных вычислений.

Замыкания

Это когда есть функция, которая внутри себя содержит переменную из внешнего контеста; ее значение захватывается и внутри функции не меняется. Пример: функция для вычисления производной. Запоминается шаг, который потом считается...


In [43]:
let derivative (dx:float) f = fun x -> (f(x + dx) - f(x)) / dx
let deriv = derivative 0.01
let косинус = deriv sin

косинус 0.


Out[43]:
0.9999833334

In [39]:
let x = 1
let f z = x + z
f 1


Out[39]:
2

Изменяемые переменные и бесконечные последовательности

Генератором называется функция, которая по числу получается элемент бесконечной последовательности.


In [49]:
let mutable x = 1
let d z = z + z
x <- 2
f 1


Out[49]:
2

In [63]:
let new_counter n =
    let mutable x = n in
    fun () ->
        x <- x + 1
        x
        
let c = new_counter 0
c()


Out[63]:
1

In [51]:
c()


Out[51]:
2

In [52]:
c()


Out[52]:
3

Видно, что в замыкании применяется уже новое значение функции.

Хотим сделать генератор общего вида. Для, например, чисел Фибоначчи, потребуется уже два счетчика.

В более общем подходе у нас есть состояния и функция, которая совершает переход от старого состояния к новому.


In [57]:
let generator f n =
    let mutable x = n in
    fun () ->
        x <- f x
        x
        
let numbers = generator ((+)1) 0
numbers ()
numbers ()


Out[57]:
2

In [58]:
let fib = generator (fun (u, v) -> (v, u+v)) (1, 1)
fib ()
fib ()


Out[58]:
(2, 3)

Немного не то, хотим еще выбрать один элемент из пары. Можно написать обертку, однако лучше использовать map.


In [64]:
let map f g = // g = generator
    fun () -> f(g()) // Применяем функцию f к новому значению генератора.
    
let fibs = fib |> map fst
fibs () // Обратим внимание, что мы не перезапускаем генератор из предыдущей ячейки, так что числа сразу со сдвигом.


Out[64]:
21

Как написать фильтр?


In [67]:
let rec filter p g =
    fun () ->
        let x = g()
        if p x
            then x
            else (filter p g)()
            
let f3 = fibs |> filter (fun x -> x % 3 = 0)
f3 () // Получаем последовательные числа Фибоначчи, делящиеся на 3.


Out[67]:
6765

Это все очень часто применяется в реальной жизни. В DotNet и Java есть понятие Enumerator - функция, которая возвращает следующий элемент.

Seq

На самом деле в F# уже есть готовые (ленивые) последовательности; написанное нами было нужно для понимания, как оно работает под капотом.


In [70]:
[1..100] // Выделяется память под 100 чисел.
{1..100} // Ничего не выделяется, лишь функция, умеющая выдывать элемент.


Out[70]:
seq [1; 2; 3; 4; ...]

In [73]:
[1..100] |> Seq.map ((*) 2) // Выделяется память под еще 100 чисел!
{1..100} |> Seq.map ((*) 2) // Память все равно не выделяется; добавляется новая функция.


Out[73]:
seq [2; 4; 6; 8; ...]

In [80]:
let fib = Seq.unfold (fun (u, v) -> Some(u, (u + v, u))) (1, 1)

fib


Out[80]:
seq [1; 2; 3; 5; ...]

In [81]:
fib |> Seq.filter (fun x -> x % 3 = 0)


Out[81]:
seq [3; 21; 144; 987; ...]

In [83]:
let numbers = Seq.unfold (fun x -> Some(x, x + 1)) 1
numbers


Out[83]:
seq [1; 2; 3; 4; ...]

In [84]:
let numbers = seq { // Никогда так нельзя писать.
    let mutable x = 1
    while true do 
        yield x // Это возвращает в Seq еще один элемент.
        x <- x + 1
}

numbers


Out[84]:
seq [1; 2; 3; 4; ...]

In [95]:
let onec = seq { // Ужасно, некрасивно, нефункционально!
    while true do
        yield 1
}

ones


Out[95]:
seq [1; 1; 1; 1; ...]

In [101]:
let rec ones = seq { // Уже красиво.
    yield 1 // Сначала возвращаем единицу.
    yield! ones // А потом эту же последовательность рекурсивно. 
}

ones


/home/nbuser/input.fsx(3,12): warning FS0040: This and other recursive references to the object(s) being defined will be checked for initialization-soundness at runtime through the use of a delayed reference. This is because you are defining one or more recursive objects, rather than recursive functions. This warning may be suppressed by using '#nowarn "40"' or '--nowarn:40'.

In [103]:
let rec numbers = seq {
    yield 1
    yield! Seq.map((+) 1) numbers
}

numbers


/home/nbuser/input.fsx(3,27): warning FS0040: This and other recursive references to the object(s) being defined will be checked for initialization-soundness at runtime through the use of a delayed reference. This is because you are defining one or more recursive objects, rather than recursive functions. This warning may be suppressed by using '#nowarn "40"' or '--nowarn:40'.

In [105]:
let fact n = [1..n] |> List.reduce (*) // Тратится много памяти.

In [106]:
let fact n = seq {1..n} |> Seq.reduce (*)

Заметим, что .. - оператор с двумя аргументами, так что частично применим его.


In [112]:
let fact = (..) 1 >> Seq.reduce (*)
fact 7


Out[112]:
5040

In [110]:
numbers |> Seq.scan (*) 1 // Самое красиво описание факториала.


Out[110]:
seq [1; 1; 2; 6; ...]

Практическое применение

Все это используется, например, при работе с памятью. Всегда нужно предполагать, что файл не помещается в оперативную память. Для этого используется чтение построчно при помещении в бесконечный список.

Смотрим пример с "Анной Карениной".


In [ ]:
let Readlines fn =
    seq {
        use inp = File.OpenText fn in
            while not(inp.EndOfStream) do
                yield (inp.ReadLine())
    }
    
Readlines @"c:\books\anna_kar.txt"
|> Seq.collect (fun x -> s.Split([' '; ',', '.', ';'])) // Стоит добавить еще раздлелителей.
|> Seq.filter (fun s -> s.Length > 0) // Здесь можно указать большую длину, чтобы избавиться от предлогов.
|> Seq.map (fun s -> s.ToLower()) // Приводим все в нижний регистр.
// |> Seq.maxBy (fun s -> s.Legnth) // Самое длинное слово.
|> Seq.groupBy (fun x -> x) // Получаем последовательность пар: (значение, [список;всех;таких;же;слов])
|> Seq.map (fun (u, s) -> fun (u, Seq.length, s)) // Теперь получили частотный словарь!
|> Seq.sortByDescending snd // Получаем самое популярное слово.

Демонстрация работы в F# с графиками на примере частотного словаря "Анны Карениной".

Можно делать анализ текста на позитивность / негативность на примере английской версии "Войны и мира". Есть список хороших и плохих слов.


In [117]:
let flip f x y = f y x

let readset f =
    Readlines f
    |> Seq.map (fub s -> s.Trim())
    |> Seq.fold (flip Set.add) Set.empty // Получили множество слов, где можно проверять принадлежность.

let pos = readset @"c:\books\positive.txt"
let neg = readset @"c:\books\negative.txt"

let wt (s:string) =
    s.Split([' '; ',', '.', ';'])
    |> Seq.fold (fun acc s ->
        if Set.contains s pos
        then acc + 1
        else if Set.cintains s neg
            then acc - 1
            else acc) 0
            
ReadLines @"c:\books\wap_1.txt"
|> Seqfilter (fun s -> s <> "")
|> Seq.scan (fun acc s ->
    if (s.Contains("CHAPTER"))
    then (s, s)
    else (acc, s))) "" ""
|> Seq.filter (func (c, _) -> c <> "")
|> Seq.groupBy fst
|> Seq.map (fun (c, s) -> (c, s |. Seq.map snd |> Seq.,ap wt |> Seq.sum)) // Пары (глава, позитивность)
|> Chart.Bar


/home/nbuser/input.fsx(5,23): error FS0010: Unexpected symbol '->' in expression

Это применяется для обработки данных, появляющихся в реальном времени. Например, анализ тональности Twitter'а.