Страницы

Поиск по вопросам

Показаны сообщения с ярлыком f#. Показать все сообщения
Показаны сообщения с ярлыком f#. Показать все сообщения

суббота, 4 января 2020 г.

Рефакторинг функций

#инспекция_кода #f#


Нужно вытащить информацию с некоторого сайта. Для разбора HTML использую F# Data:
HTML Parser (HTML Type Provider, к сожалению, в данном случае неприменим). 

Реализовал следующим образом:

let getNextLink (document : HtmlDocument) = 
    document.Descendants "a"
    |> Seq.choose
        (fun node ->
            match node.TryGetAttribute "href" with
            |Some href when node.InnerText().Trim() = "ключевое слово" -> 
               href.Value() |> Some
            |_ -> None)
    |> Seq.tryHead

let getAllValues start  = 
    let rec loop (pages : string) = seq {
        let result = HtmlDocument.Load pages
        yield 
            result.Descendants "div"
            |> Seq.filter
                (fun node -> 
                    match node.TryGetAttribute "id" with
                    |Some id -> id.Value().StartsWith("текст для проверки")
                    |None -> false)
            |> Seq.map
                (fun node -> node.InnerText())
        let next = getNextLink result
        if next.IsSome then
            yield! loop next.Value

    }
    loop start

let path = "http://адрес.html"

let values =
    getAllValues path
    |> Seq.concat


остальная часть связана с обработкой полученных данных и к текущему вопросу отношения
не имеет.
Интересует как правильность выбора библиотеки для парсинга, так и сама реализация.
    


Ответы

Ответ 1



В целом функционально. Но если вы стремитесь к совершенству, то вот несколько советов: Вся обработка происходит как бы вместе, в один присест. Весь код находится в одной большой функции с подфункциями, и это делает код гораздо менее читаемым и запутанным. Я обычно стараюсь разбить код на много мелких функций, и потом их друг с другом стыковать. Идея в том, что каждая функция должна читаться на одном уровне, а не на нескольких. Например "для каждого А вычислить Б", а не "для каждого А, где А - это все X из Y, для которых выполняется Z, вычислить Б, где Б - это P или Q в зависимости от I + J". Человечский мозг очень плохо справляется с частым переключением контекста. Везде используются методы классов, что затрудняет вывод типов, заставляет указывать типы явно, заставляет использовать лямбда-выражения. Я обычно стараюсь оборачивать методы классов в мелкие функции, которые потом можно легко стыковать. Очень мало параметризации, все строки зашиты в код. Если строки "id", "a" и "href" ещё можно понять как часть стабильного стандарта, то строки "ключевое слово" и "текст для проверки" совершенно точно должны быть параметрами. Основной цикл у вас выдаёт последовательность последовательностей - seq>, и потом вы её склеиваете с помощью Seq.concat. Эта операция видится мне лишней: раз уж вы используете выражение seq { }, то можно внутри него сразу разворачивать последовательности используя yield! вместо yield. Здесь я не уверен, возможно это у вас такое требование, но: что происходит, если на странице есть несколько ссылок с текстом "ключевое слово"? Ваша функция getNextLink вернёт только первую ссылку, а остальные вы теряете. Можно было бы совсем без затрат устроить обработку всех ссылок, а не только первой, просто убрав Seq.tryHead из getNextLink. Но я не уверен, что это будет "лучше" в вашей ситуации. Вот что у меня получилось в результате применения этих советов: let attr name (node: HtmlNode) = node.TryGetAttribute name |> Option.map (fun v -> v.Value()) let text (node: HtmlNode) = node.InnerText() let trim (s: string) = s.Trim() let descendants tag (node: #HtmlNode) = node.Descendants tag let startsWith prefix (s: string) = s.StartsWith prefix let hasText value node = trim (text node) = value let hasId value node = attr "id" |> Option.exists (startsWith value) let getNextLink text = descendants "a" >> Seq.filter (hasText text) >> Seq.choose (attr "href") >> Seq.tryHead let linksOnPage idPrefix doc = descendants "div" >> Seq.filter (hasId idPrefix) >> Seq.map text let getAllValues linkText idPrefix start = let rec linksFromPageTree (url : string) = seq { let doc = HtmlDocument.Load url yield! linksOnPage idPrefix doc yield! nextSubTree doc } and nextSubTree doc = match getNextLink linkText doc with | None -> Seq.empty | Some nextUrl -> linksFromPageTree nextUrl linksFromPageTree start let path = "http://адрес.html" let values = getAllValues "ключевое слово" "текст для проверки" path

четверг, 26 декабря 2019 г.

Об алгоритме вывода типов рекурсивных функций

#рекурсия #scala #f# #ocaml #теория_типов


В Scala не реализован вывод типов для рекурсивных функций, в качестве аргумента Кей
Хорстман в своей книжки пишет что "Алгоритм Хиндли-Милнера не стабильно себя ведёт
в языках с ООП". Но, например, в F# и OCaml реализован вывод типов для рекурсивных
функций и возникают вопросы: что же тогда имел ввиду Кей Хорстман? Есть ли примеры
программ на ООП языке в которых алгоритм даёт сбой(ошибка согласованности типов в рантайме
при успешно пройденной проверке типов на этапе компиляции)? Или косяк именно в каких-то
особенностях Scala?
    


Ответы

Ответ 1



Система типов Scala однозначно содержит F \sub. Вероятно содержит намного больше (это сейчас активное направление исследований у нас) но уже в F \sub многие вопросы, и в том числе этот - неразрешимая задача. Ссылка на статью Если честно, я не знаю как это работает в F# и OCaml. Думаю они делают best-effort.

воскресенье, 15 декабря 2019 г.

Получение информации о функциях в модуле

#net #рефлексия #f#


Заинтересовал вопрос как получить информацию о функциях которые содержит модуль. 
Хочу получить что-то вроде того, что выдает FSI.

Например для следующего модуля

module Test = 
    let ign _ = ()
    let getNowDateTime() = System.DateTime.Now
    let getNumbers count = [1..count]


FSI выводит такую информацию

module Test = begin
  val ign : 'a -> unit
  val getNowDateTime : unit -> System.DateTime
  val getNumbers : count:int -> int list
end


Пробовал достать данные через рефлексию следующим образом

let getInfoAboutModule (t : Type) = 

    let genericToString (t : Type) = 
        match t.GenericTypeArguments with
        | [| |] -> t.FullName
        | x -> 
            x 
            |> Seq.map (fun x -> x.Name)
            |> String.concat "," 
            |> sprintf "%s<%s>" t.Name

    let getInfo (mi:MethodInfo) =
        let parameter = 
            let sb = System.Text.StringBuilder()

            for x in mi.GetParameters() do
                x.ParameterType
                |> genericToString
                |> sprintf "%s : %s ->" x.Name
                |> sb.Append
                |> ignore

            sb.ToString()
            |> fun str -> if str |> System.String.IsNullOrEmpty then "unit -> " else str

        sprintf "%s : %s %s" mi.Name parameter (genericToString mi.ReturnType) 

    t.GetMethods(BindingFlags.Public ||| BindingFlags.Static)
    |> Seq.map getInfo


для модуля Test результат будет следующим

ign : _arg1 :  -> System.Void
getNowDateTime : unit ->  System.DateTime
getNumbers : count : System.Int32 -> FSharpList`1


MCVE (ideone)

но, естественно, таким образом получаю названия далекие от F#-ных.



P.S. Добавлять какие-либо атрибуты к модулю нельзя, т.к. нужно оставить возможность
извлекать данные из модулей из других сборок.
    


Ответы

Ответ 1



Можно воспользоваться FSharp.Compiler.Service для анализа исходных текстов модулей. Если модуль скомпилирован и его исходных текстов нет, то сомневаюсь, что можно добиться лучшего результата, чем ваш. На основе этого примера: open System open System.IO open Microsoft.FSharp.Compiler.SourceCodeServices let text = """ module Test = let ign _ = () let getNowDateTime() = System.DateTime.Now let getNumbers count = [1..count] """ let checker = FSharpChecker.Create() // из примера let parseAndTypeCheckSingleFile input = let file = "test.fsx" let projOptions = checker.GetProjectOptionsFromScript(file, input) |> Async.RunSynchronously let parseFileResults, checkFileResults = checker.ParseAndCheckFileInProject(file, 0, input, projOptions) |> Async.RunSynchronously // Wait until type checking succeeds (or 100 attempts) match checkFileResults with | FSharpCheckFileAnswer.Succeeded(res) -> parseFileResults, res | res -> failwithf "Parsing did not finish... (%A)" res // печать информации о члене модуля let printInfo (fn:FSharpMemberOrFunctionOrValue) = // строка с информацией о типе let rec getTypeString (t:FSharpType) = if t.HasTypeDefinition then let prms = [ for x in t.GenericArguments -> (getTypeString x) ] in t.TypeDefinition.DisplayName + " " + String.Join(" ", prms) else t.ToString() printf "%s: " fn.DisplayName for group in fn.CurriedParameterGroups do for prm in group do match prm.Name with | None -> () | Some name -> printf "%s:" name printf "%s -> " (getTypeString prm.Type) printf "%s\n" (getTypeString fn.ReturnParameter.Type) [] let main argv = let parseFileResults, checkFileResults = parseAndTypeCheckSingleFile text let m = checkFileResults.PartialAssemblySignature.Entities.[0] let fns = m.NestedEntities.[0].MembersFunctionsAndValues for x in fns do printInfo x 0 Результат: ign: type 'a -> unit getNowDateTime: unit -> DateTime getNumbers: count:int -> list int Либо можно поизучать исходники непосредственно FSI. Если возникает ошибка при парсинге, нужно скопировать файлы FSharp.Core.optdata и FSharp.Core.sigdata в каталог с программой. Файлы можно найти в %PROFRAMFILES%\Reference Assemblies\Microsoft\FSharp\.NETFramework\v4.0\4.4.0.0\.

среда, 27 ноября 2019 г.

Как же можно программировать без переменных? Вопрос о функциональщине)

#scala #haskell #f#


Пока занимаюсь повторением Python и С, и все никак не дойдут руки до Haskell. А вопрос
не покидает голову уже долгое время) Может как то вкратце можно на него ответить?
    


Ответы

Ответ 1



Трагедия современных программистов заключается в том, что, изучив школьную математику, и приступая к программированию, они пытаются понять, почему запись x = x + 1 имеет смысл. Когда они это понимают, им сложно научиться мыслить «как раньше». Дмитрий Сошников говорит, что в это время в них умирает функциональный программист. Тем не менее, вспомнить можно, и это не так трудно. Попробуйте решить любое уравнение на бумаге и вы сделаете это в функциональном стиле. Попробуйте вычислить факториал. Вы увидите, что вам не нужна переменная-аккумулятор. Примером переменной-аккумулятора являются классические счёты, и там действительно возникает такое понятие, как состояние. Функциональная запись настолько привычна, что она присутствует во всех императивных языках, кроме некоторых эзотерических. Посмотрите на присваивание g = a * b + c * d + e * f; и скажите, в каком порядке будут выполняться умножения в правой части? Сначала a * b, в конце e * f, или наоборот? a * b и c * d это операнды оператора +, и в таких языках, как C и C++ порядок вычисления операндов не определён. Благодаря этому, компилятор может оптимизировать код. Например, если где-то выше c * d уже вычислили, и значение переменных не поменялось, можно подставить готовое произведение. То, что порядок вычисления не определён, свидетельствует о функциональном стиле. Это он и есть. Если вы захотите вычислить выражение императивно, вам придётся разбить его шаги, или писать на языках Fort или PostScript. Наконец, ещё один способ, «почуствовать» вкус функционального стиля, это записывать вычисления в Excel. Например, не пользуясь функцией SIN сделайте лист для расчёта синусов. Это занимательное задание. Здесь тоже вы определяете только зависимости между ячейками, но не определяете, в каком порядке они вычисляются. Excel, конечно, не Тьюринг-полный вычислитель, тем не менее, он позволяет решить многие практические задачи.

Ответ 2



Циклы Для реализации циклических алгоритмов, как вы совершенно верно заметили, в функциональном программировании используется рекурсия. Например: map f [] = [] map f (x:xs) = f x : map f xs Функция map проходит по всему списку и преобразовывает каждый элемент с помощью функции f. Довольно просто. На практике, однако, рекурсия непосредственно используется редко. Как правило функциональные программы выражают циклические операции через уже существующие библиотечные примитивы, такие как функция map приведённая выше. Что-то в таком роде: numbers = [1,2,3,4,5] strings = show <$> numbers Здесь оператор <$> - это псевдоним функции map (стандартный в Haskell), так что выражение show <$> numbers читается как "применить функцию show к каждому элементу списка numbers". Для более сложных случаев существуют более общие аналоги, такие как fold, for, mapM и т.п. Самые простейшие реализованы с помощью рекурсии, а более сложные строятся на простейших. Мутация "Мутацией" в функциональном программировании (и вообще в программировании) называется изменение содержимого ячейки памяти. В языке Haskell мутация в принципе возможна (см. ниже), но на практике её стараются избегать, прибегая к ней лишь в самых крайних случаях. Вместо непосредственного изменения данных, в функциональном программировании принято создавать новые структуры на основе старых. Например: replaceFirst (x:xs) newX = newX : xs Функция replaceFirst "заменяет" первый элемент списка на новый: xs = [1,2,3,4] ys = replaceFirst xs 42 -- Теперь ys = [42,2,3,4] (внимательный читатель заметит, что функция replaceFirst неприменима к пустому списку, но это не относится к текущей дискуссии) Логически операцию replaceFirst xs 42 можно понимать как "заменить первый элемент списка xs на 42", но на практике происходит не совсем это. На самом деле старый список xs жив и здоров, а список ys - это совершенно новый список. Теперь у нас есть выбор: мы можем выкинуть список xs за ненадобностью, чтобы сборщик мусора его потом удалил, или мы можем оставить его "про запас", на будущее. Такой подход открывает очень широкие возможности. Например, нам ничего не стоит хранить историю изменений - или просто для учёта, или с целью вернуться назад во времени, или ещё для чего. Более интересная возможность - распараллеливание программы. Если мы уверены, что старые данные никогда не изменятся, то нет смысла городить семафоры и прочую синхронизацию - можно просто запустить программы параллельно, и всё! - компилятор гарантирует, что они никогда не будут мешать друг другу модифицируя общие данные. Синхронизация данных - краеугольный камень параллельного программирования и основной источник багов. Если вы в этом сомневаетесь, поищите "defensive copy". Q: Вы что же, хотите сказать, что при каждом изменении надо выделять целую новую структуру данных?! Эдак же никакой памяти не напасёшься! Нет, не нужно. Для того, чтобы этого не происходило, в функциональном программировании используются специальные структуры данных, называемые по-английски "persistent data structures". Это такие структуры данных, в которых каждое следующее обновление построено на основе предыдущей версии. Самый простой пример - односвязный список. Представьте себе список: каждый элемент содержит ссылку (указатель) на предыдущий, а самый последний - ссылку на "конец". Как-то так: (1) --> (2) --> (3) --> (4) --> (end) В примере с replaceFirst (см. выше) этот список у меня назывался xs: (1) --> (2) --> (3) --> (4) --> (end) xs _/ Когда я вызвал функцию replaceFirst, она взяла "хвост" списка (начиная с двойки) и присоединила к нему новую "голову" 42. Этот "новый" список я назвал ys: ys _ \ (42)_ \ (1) --> (2) --> (3) --> (4) --> (end) xs _/ Отсюда видно, что для того, чтобы "заменить" первый элемент списка, вовсе не нужно выделять новый блок памяти. Единственная новая ячейка - это сам новый первый элемент, и всё. Все остальные элементы остались на месте. Более того: если я теперь решу, что список xs мне больше не нужен, то сборщику памяти придётся убирать только число 1, поскольку все остальные элементы списка всё ещё задействованы. Также обратите внимание на то, что такой подход возможен только тогда, когда точно известно, что элементы списка никогда не будут изменены. Только при этом условии я могу рассматривать списки xs и ys как два разных списка, хотя они и занимают частично одну и ту же память. Конечно, односвязный список - это самая простая структура данных. В более сложных случаях используются более сложные структуры - в основном деревья всех видов и размеров. И разумеется, есть ситуации, при которых такой подход всё же менее эффективен, чем непрерывный массив в памяти. Однако в 99% случаев разница в быстродействии совершенно ничтожна и не идёт ни в какое сравнение с повышенной стабильностью и простотой разработки. Для оставшегося 1% случаев, однако, даже Haskell позволяет работать с непрерывными массивами - только делает он это гораздо более безопасным образом (см. ниже) Императивные программы "Императивными" называются программы, выраженные в виде последовательности шагов. Это в противовес "декларативным", которые выражены в виде отношений между частями - функциями и данными. Все примеры, приведённые мной выше - "декларативные". Они все написаны в стиле "xs - это ys, где первый элемент заменён на 42" и т.п. На первый взгляд может показаться, что разницы в принципе нет, но это не так. Главная разница в том, что в императивной программе есть порядок действий, а в декларативной - нет. Я указал, как значение xs связано с ys, но не указал, в каком порядке мне хотелось бы, чтобы эта связь вычислялась. Компилятор разберётся за меня сам. Но это не всегда целесообразно. Хотя "в принципе" любую программу можно выразить и тем и другим способом, иногда чисто с человечксой точки зрения удобнее думать о программе как о функциональных отношениях, а иногда - как о последовательности шагов. Например, рассмотрим ту же игру. Представьте, что у меня есть некий тип данных Game и набор операций к нему: type Game = ... lookForMonsters :: Game -> (Game, Monster) shoot :: Monster -> Game -> (Game, Damage) Обратите внимание, что каждая функция, в дополнение к своему "основному" результату (Monster или Damage), возвращает ещё и "новое", изменённое состояние игры. Это "новое" состояние должно быть передано в следующую функцию, за счёт чего и возникает определенный порядок исполнения этих функций, как-то так: game0 = createGame (game1, monster) = lookForMonsters game0 (game2, damage) = shoot monster game1 message = "Inflicted " ++ show damage ++ " points of damage Поскольку значение game1 необходимо для вызова shoot, но является результатом lookForMonsters, выходит, что lookForMonsters нужно вызвать сначала, а shoot - потом. В этом и есть определение порядка вызовов. Такой стиль, однако, весьма утомителен и создаёт опасность опечаток. Кроме того, обратите внимание на закономерность: все строчки очень похожи друг на друга по форме. Функциональный программист живёт закономерностями. Он их находит и безжалостно обобщает, чтобы сделать свой код более простым и понятным. В данном случае давайте завернём эту закономерность в пару функций, которые я назову bind и return: bind x f = \game -> let (game1, y) = x game in f y game1 return x = \game -> x Не вдавайтесь слишком глубоко в подробности, если это непонятно. Достаточно знать, что функция bind "связывает вместе" две операции над игрой, передавая состояние игры из результата первой операции в аргумент второй; а функция return создаёт операцию, которая игнорирует состояние игры и возвращает какое-то конкретное значение. С помощью этих функций я могу записать мою программу так: program = bind lookForMonsters (\monster -> bind (shoot monster) (\damage -> return ("Inflicted " ++ show damage ++ " points of damage) ) ) (finalGameState, message) = program game0 Можно сделать чуть красивее, поиграв с переводами строк и отступами: program = bind lookForMonsters (\monster -> bind (shoot monster) (\damage -> return ("Inflicted " ++ show damage ++ " points of damage))) Или ещё чуть красивее, если завести оператор >>=, который будет синонимом для bind: x >>= f = bind x f program = lookForMonsters >>= \monster -> shoot monster >>= \damage -> return ("Inflicted " ++ show damage ++ " points of damage) Такая схема определения порядка исполнения называется "монад", и она настолько широко применяется в функциональных языках, что большинство компиляторов предоставляет для неё специальный синтаксический сахар. В Haskell эту программу можно записать так: program = do monster <- lookForMonsters damage <- shoot monster return ("Inflicted " ++ show damage ++ " points of damage) Слово do и стрелки влево <- преобразуются компилятором в последовательные вызовы функции bind. После всех этих преобразований, посмотрите, что у нас получилось: программа выглядит как "обычная" императивная программа на каком-то языке вроде C или Python, и смотрится так, как будто мы "изменяем" состояние игры с каждым шагом. Но на самом деле программа целиком состоит из "чистых" функций без мутации и побочных эффектов. Наша программа принимает изначальное сотояние игры на вход и выдаёт конечное состояние на выходе, и не зависит больше ни от чего. Таким образом мы получили возможность писать программу в виде последовательности шагов, но при этом сохранили все преимущества отсутствия мутации. Обобщение Взгляните ещё раз на программу, записанную мной выше в "хитром" виде: program = do monster <- lookForMonsters damage <- shoot monster return ("Inflicted " ++ show damage ++ " points of damage) Обратите внимание, что в этой программе нигде не упоминается сам тип Game - то есть сама программа в общем-то не в курсе, что она работает с игрой. Всё знание об игре "спрятано" в функции bind и в примитивных операциях lookForMonsters и shoot. Кстати, сами эти операции тоже могут и не быть примитивными - они сами по себе могут быть записаны в таком же стиле, как и моя программа, и основаны на ещё более примитивных операциях. Но это просто к слову. А следующая мысль вот какая: если сама по себе программа не зависит от того, как именно реализована идея "порядка операций" (а именно эту идею реализует функция bind), значит мы можем изменить эту реализацию не трогая при этом саму программу. Наша реализация может быть простой и "игрушечной", как я привёл выше, или, если нам понядобятся какие-то дополнительные возможности, например прерывание при ошибках или журналирование, - мы можем добавить эти возможности в функцию bind, и программа этого не заметит. Эта мысль приводит к идее, что "монады" могут быть разные. Более того, монады можно собирать из блоков с нужной функциональностью, как из кубиков, и при этом писать программы, в которых каждая операция пользуется только теми кубиками, которые ей важны, а остальная программа о них не знает. Ввод-вывод И вот теперь, когда у нас есть понятие "монад", который есть абстрактное выражение порядка операций, приходит следующая мысль: а что если изобрести такой монад, который написан не на самом языке Haskell, а реализован внутри компилятора? И что если организовать этот монад таким образом, что функция bind может творить всякие безобразия, нарушать правила, и вообще быть написана на языке C? Программы, написанные в таком монаде, будут выглядеть совершенно невинно - как последовательность функций, пропущенных через bind, так же, как и примеры выше. Но зато примитивные операции будут для реального ввода-вывода. Вот, например, операция getLine: getLine :: IO String Она производит "побочный эффект" в реальном мире - читает текстовую строку с консоли. Но эта строка "завёрнута" в тип IO. Логически можно это рассматривать как ячейку, в которой находится строка, но "достать" эту строку оттуда никак нельзя. Единственное, что можно с ней сделать - это направить её в качестве параметра в другую функцию с помощью вызова bind. Но вот незадача: в результате этого вызова опять обязательно получится тип IO (возможно не со строкой внутри, а с чем-то другим), и оттуда достать значение опять нельзя. Можно только направить его в третью функцию опять с помощью bind - и так далее. В результате выходит, что как только мы произвели ввод-вывод (т.е. вызвали одну из IO-функций), единственное, что можно сделать с результатом - это произвести ещё ввод-вывод, и ещё, и ещё. Но это как раз ничего страшного, потому что точка входа программы на Haskell - это как раз и есть цепь операций ввода-вывода, скреплённых через bind. Например: main = bind getLine (\name -> bind (putStrLn ("Hello, " ++ name ++ "!")) (\_ -> return 0 ) ) Или то же самое с использованием синтаксиса do: main = do name <- getLine putStrLn ("Hello, " ++ name ++ "!") return 0 Вот таким образом и происходит ввод-вывод в Haskell. Самые примитивные операции, такие как getLine и putStrLn, написаны не на самом Haskell, а на C. Это примерно так же, как вызов CreateProcess в Windows или fork в UNIX реализованы не самой программой, а операционной системой. Самые примитивные примитивы всегда находятся на один уровень ниже, без этого никак. Альтернативный взгляд на эту структуру такой: можно рассматривать программу на Haskell как программу, которая сама по себе ввода-вывода не делает, но зато генерирует программу на другом языке, которая уже делает ввод-вывод. Я не большой фанат такого подхода, но его часто используют при объяснениях. Обратите внимание, что, в принципе, можно абсолютно всю программу на Haskell написать непосредственно в IO, и таким образом пользоваться всеми теми же "благами", что и программа на C или Python. Однако это совершенно убьёт всю идею. Смысл языка Haskell в том, что программы можно писать гораздо более безопасные, выразительные и быстрые, если только отказаться от всяких безобразий типа IO и дать компилятору возможность оптимизировать программу. Поверьте, компилятор (особенно компилятор Haskell) справляется с этим на много порядков лучше нас с вами. Поэтому реальные программы обычно написаны в два слоя: самое ядро программы, где вся логика и все вычисления, написано на "чистых" функциях, и только самая внешняя, очень тонкая оболочка имеет дело с IO. В настоящее время этот принцип, называемый по-английски "functional core, imperative shell", стал широко известен и за пределами функционального программирования - как, впрочем, и многие другие функциональные идеи (например, LINQ, const, then, async, auto и т.п.) "Настоящая" мутация Теперь, когда мы познакомились с монадом IO и узнали, что его примитивы могут творить всякие безобразия, следующее что приходит в голову - что эту лазейку можно использовать в случаях, когда Haskell таки недостаточно быстр, требует слишком много памяти, или не устраивает по каким-то другим причинам. В этих случаях можно написать библиотеку на C и импортировать её в Haskell через IO. Один пример такого подхода - массивы с поддержкой "настоящей" мутации. Эта библиотека предоставляет IO-примитивы для создания, изменения и прочих операций над массивами: main = do arr <- newArray (1,10) 42 writeArray arr 8 5 a <- readArray arr 1 print a Как я уже отметил выше, такие возможности используются только в самых крайних случаях - когда Haskell сам по себе "не дотягивает". Я бы сравнил это с написанием программы на C, но с вкраплениями ассемблера в самых чувствительных местах. Программа получается больше, непонятнее, более хрупкой, и ограничивает возможность оптимизации, так как компилятор не знает, что именно происходит внутри этих примитивов, и потому боится их трогать.

Ответ 3



Программировать без переменных - невозможно. Во первых, если мы хотим изменить во всём скрипте одно значение, то нам придётся во всей программе изменять это значение (а в скрипте могут быть тысячи, десятки тысяч строк). Во вторых, если нам нужно использовать знаки действий (+, -, *, :), то без переменных не обойтись.

четверг, 29 ноября 2018 г.

Об алгоритме вывода типов рекурсивных функций

В Scala не реализован вывод типов для рекурсивных функций, в качестве аргумента Кей Хорстман в своей книжки пишет что "Алгоритм Хиндли-Милнера не стабильно себя ведёт в языках с ООП". Но, например, в F# и OCaml реализован вывод типов для рекурсивных функций и возникают вопросы: что же тогда имел ввиду Кей Хорстман? Есть ли примеры программ на ООП языке в которых алгоритм даёт сбой(ошибка согласованности типов в рантайме при успешно пройденной проверке типов на этапе компиляции)? Или косяк именно в каких-то особенностях Scala?


Ответ

Система типов Scala однозначно содержит F \sub. Вероятно содержит намного больше (это сейчас активное направление исследований у нас) но уже в F \sub многие вопросы, и в том числе этот - неразрешимая задача.
Ссылка на статью
Если честно, я не знаю как это работает в F# и OCaml. Думаю они делают best-effort.