Функційна мова максимально адаптована до використання в .NET , відповідно, вона не заперечує і імперативного підходу.
Протягом тривалого часу F# існував як дослідницький проект, основним завданням якого було збагатити імперативну мову C# можливостями традиційно доступними лише функціональним мовам. Безліччю нововведень C# 3.0 з Visual Studio 2008 завдячує саме йому. Сам по собі F# не створений з чистого аркуша в Microsoft, в його основу покладено досить популярний OCaml, який, у свою чергу, походить від одної з перших типізованих функціональних мов ML. Попри те, що синтаксично F# і OCaml досить близькі, вони не еквівалентні: грубо кажучи, перший становить собою підмножину другого, доповнену доступом до властивостей .NET Framework. Однак деякі програми на OCaml можуть бути практично без модифікацій скомпільовані F#, зворотна компіляція також можлива, зрозуміло, за відсутності звернень до класів .NET Framework.
Історія
Історія F# почалася в 2002 році, коли команда розробників з Microsoft Research під керівництвом Don Syme вирішила, що мови сімейства ML цілком підходять для реалізації функціональної парадигми на платформі .NET. Ідея розробки нової мови з'явилася під час роботи над Generic'ами — реалізацією узагальненого програмування для Common Language Runtime. Відомо, що у свій час як прототип нової мови розглядалася Haskell, але через функціональну чистоту і складнішу систему типів потенційний Haskell.NET не міг би надати розробникам простого механізму роботи з бібліотекою класів .NET Framework, а значить, не давав би якихось додаткових переваг. Як би там не було, за основу був узятий OCaml, мова з сімейства ML, яка не є чисто функціональною і надає можливості для імперативного і об'єктно-орієнтованого програмування. Haskell, хоч і не став безпосередньо батьком нової мови, тим не менше, справив на нього деякий вплив. Наприклад, концепція обчислювальних виразів (computation expressions або workflows), що грають важливу роль для асинхронного програмування та реалізації DSL на F#, запозичена з монад Haskell.
Наступним кроком у розвитку нової мови стала поява в 2005 році її першої версії. З тих пір навколо F# стало формуватися співтовариство. За рахунок підтримки функціональної парадигми мова виявилася потрібною в науковій сфері та фінансових організаціях. Багато в чому завдяки цьому Microsoft вирішила перевести F# зі статусу дослідницьких проектів у статус підтримуваних продуктів і поставити її в один ряд з основними мовами платформи .NET. І це попри те, що останнім часом все більшу активність проявляють динамічні мови, підтримка яких також присутня в .NET Framework. 12 квітня 2010 світ побачила нова версія флагманського продукту для розробників — Microsoft Visual Studio 2010, яка підтримує розробку на F# прямо з коробки.
Основні можливості мови
F# є багатопарадигмальною мовою, а це означає, що на ній можна реалізовувати функції вищих порядків, усередині яких виконувати імперативний код і обгортати все це в класи для використання клієнтами, написаними іншими мовами на платформі .NET.
F# функціональна
F#, будучи спадкоємцем традицій сімейства мов ML, надає повний набір інструментів функціонального програмування: тут є алгебричні типи даних і функції вищого порядку, можливість композиції функцій і незмінні структури даних, а також часткове застосування на пару з карруванням. Зі слів експертів в OCaml, в F# не вистачає функціональних обʼєктів.
Усі функціональні можливості F # реалізовані поверх загальної системи типів .NET Framework. Однак цей факт не забезпечує зручності використання таких конструкцій з інших мов платформи. При розробці власних бібліотек на F# слід передбачити створення об'єктно-орієнтованих обгорток, які буде простіше використовувати з C# або Visual Basic. NET.
F# імперативна
У випадках, коли багатих функціональних можливостей не вистачає, F # надає розробникові можливість використовувати в коді принади змінюваного стану. Це як безпосередньо змінювані змінні, підтримка полів і властивостей об'єктів стандартної бібліотеки, так і явні цикли, а також змінні колекції і типи даних.
F# об'єктно-орієнтована
Об'єктно-орієнтовані можливості F #, як і багато чого іншого в цій мові, обумовлені необхідністю надати розробникам можливість використовувати стандартну бібліотеку класів .NET Framework. З поставленим завданням мова цілком справляється: можна як використовувати бібліотеки класів, реалізовані на інших .NET мовах, так і розробляти свої власні. Слід зазначити, однак, що деякі можливості ГО мов реалізовані не самим звичним чином.
Таке змішання парадигм, з одного боку, може привести до плачевних результатів, а з іншого — надає більше гнучкості і дозволяє писати простіший код. Так, наприклад, всередині стандартної бібліотеки F# деякі функції написані в імперативному стилі з метою оптимізації швидкості виконання.
Томаш Петрічек у своєму блозі згадує також про «мовно-орієнтоване» програмування. F# відмінно підходить як для написання вбудованих DSL, тобто імітації предметної області засобами мови програмування, так і для перетворення F# коду в конструкції, виконувані іншими засобами, наприклад в SQL-вирази або в послідовності інструкцій GPU. Крім того, в комплект постачання F# входять утиліти FsYacc і FsLex, які є аналогами таких же утиліт для OCaml, і які дозволяють генерувати синтаксичні та лексичні аналізатори, а значить на F# цілком можна розробити свою власну мову програмування.
Розширені можливості
Обчислювальні вирази
Серед нововведень F# можна особливо виділити так звані обчислювальні вирази (computation expressions або workflows). Вони є узагальненням генераторів послідовностей і, зокрема, дозволяють вбудовувати в F# такі обчислювальні структури, як монади і моноїд. Також вони можуть бути застосовані для асинхронного програмування та створення DSL.
Обчислювальний вираз має форму блоку, що містить певний код на F# у фігурних дужках. Цьому блоку повинен передувати спеціальний об'єкт, який називається ще будівник (builder). Загальна форма наступна: builder {comp-expr}.
Приклад написання обчислювального виразу для роботи з послідовностями, дозволяє створювати та маніпулювати послідовностями в функціональному стилі:
letmySequence=seq{yield1yield2yield3}
Будівник визначає спосіб інтерпретації того коду, який вказаний у фігурних дужках. Сам код обчислення зовні майже не відрізняється від звичайного коду на F#, крім того, що в ньому не можна визначати нові типи, а також не можна використовувати змінні. Замість таких значень можна використовувати посилання, але робити це слід з великою обережністю, оскільки обчислювальні вирази зазвичай створюю відкладені обчислення, а останні не дуже люблять побічні ефекти.
У залежності від будівника всередині обчислювального виразу можна використовувати особливі конструкції let!, Use!, Return, return!, Yield і yield!. Якщо читач знайомий з мовою програмування Haskell, то можна помітити, що let! відповідає стрілкою з нотації do, а return має той же зміст, що і в Haskell.
За своєю суттю обчислювальне вираз є синтаксичним цукром навколо зазначеного будівника. Компілятор F# проходиться по коду і замінює мовні конструкції викликами відповідних методів будівника b згідно з наступною таблицею:
Конструкція
Форма перетворення
let pat = expr in cexpr
let pat = expr in cexpr
let! pat = expr in cexpr
b.Bind(expr, (fun pat -> cexpr))
return expr
b.Return(expr)
return! expr
b.ReturnFrom(expr)
yield expr
b.Yield(expr)
yield! expr
b.YieldFrom(expr)
use pat = expr in cexpr
b.Using(expr, (fun pat -> cexpr))
use! pat = expr in cexpr
b.Bind(expr, (fun x -> b.Using(x, fun pat -> cexpr))
do! expr in cexpr
b.Bind(expr, (fun () -> cexpr))
for pat in expr do cexpr
b.For(expr, (fun pat -> cexpr))
while expr do cexpr
b.While((fun () -> expr), b.Delay( fun () -> cexpr))
if expr then cexpr1 else cexpr2
if expr then cexpr1 else cexpr2
if expr then cexpr
if expr then cexpr else b.Zero()
cexpr1
cexpr2
b.Combine(cexpr1, b.Delay(fun () -> cexpr2))
try cexpr with patn -> cexprn
b.TryWith(expr, fun v -> match v with (patn:ext) -> cexprn | _ raise exn)
try cexpr finally expr
b.TryFinally(cexpr, (fun () -> expr))
Основна ідея полягає в тому, що коли компілятор обробляє конструкцію обчислювального виразу, то він намагається викликати відповідний метод будівника. Будівник не зобов'язаний реалізовувати всі вказані методи. Якщо потрібного методу немає, то буде згенерована помилка компіляції.
Асинхронні потоки операцій (async workflows)
Асинхронні потоки операцій — це один з найцікавіших прикладів практичного використання обчислювальних виразів. Код, що виконує будь-які неблокуючі операції введення-виведення, як правило складний для розуміння, оскільки представляє з себе безліч callback-методів, кожен з яких обробляє якийсь проміжний результат і можливо починає нову асинхронну операцію. Асинхронні потоки операцій дозволяють писати асинхронний код послідовно, не визначаючи callback-методи явно. Для створення асинхронного потоку операцій використовується блок async:
open System.IO open System.Net open Microsoft.FSharp.Control.WebExtensions let getPage (url: string) = async {let req = WebRequest.Create (url) let! res = req.AsyncGetResponse () use stream = res.GetResponseStream () use reader = new StreamReader (stream) let! result = reader.AsyncReadToEnd () return result
}
Тут ми оголосили функцію getPage, яка повинна повертати вміст сторінки за заданою адресою. Ця функція має тип string--> Async <string> і повертає асинхронну операцію, яка може бути використана для отримання рядка з вмістом сторінки. Варто відзначити, що класи WebRequest і StreamReader не мають методів AsyncGetResponse і AsyncReadToEnd, це методи розширення.
Будівник асинхронного потоку операцій, працює таким чином. Зустрівши оператор let! або do!, він починає виконувати операцію асинхронно, при цьому метод, початківець асинхронну операцію, отримає частину, що залишилася блоку async як функція зворотного виклику. Після завершення асинхронної операції переданий callback продовжить виконання асинхронного потоку операцій, але, можливо, вже в іншому потоці операційної системи (для виконання коду використовується пул потоків). У результаті код виглядає так, як ніби він виконується послідовно. Те, що може бути з легкістю записано всередині блоку async з використанням циклів і умовних операторів, досить складно реалізується з використанням звичайної техніки, що вимагає опису множини callback-методів і передачею стану між їх викликами.
Обробка винятків — найбільш наочний приклад зручності асинхронних потоків операцій. Якщо ми пишемо асинхронний код у традиційному стилі, то кожен метод зворотного виклику повинен обробляти винятки самостійно. Блок async може включати оператор try, за допомогою якого можна обробляти винятки.
_ as e--> printfn "error:% s" e.Message return None
}
У цьому прикладі потік операцій повертає значення типу string ~ option, тобто, або рядок або пусте значення, щоб викликає код міг обробити помилку.
Значення типу Async <_> потрібно передати в один із статичних методів класу Async, щоб розпочати виконання відповідного потоку операцій. У найпростішому випадку можна скористатися методом Async.RunSynchronously, який просто заблокує викликає потік до тих пір, поки всі операції не будуть виконані.
MailboxProcessor
MailboxProcessor — це клас зі стандартної бібліотеки F #, реалізує один з патернів паралельного програмування. MailboxProcessor є агентом23, обробляють чергу повідомлень, які поставляються йому ззовні за допомогою методу Post. Вся конкурентність підтримується реалізацією класу, який містить чергу з можливістю одночасного запису кількома письменниками і читання одним єдиним читачем, яким є сам агент.
Вище наведена реалізація найпростішого агента, який при отриманні повідомлення, що містить рядок, виводить його на екран. Послати агенту повідомлення, як вже було сказано вище, можна за допомогою методу Post:
agent.Post ("Hello world!")
Цікаво відзначити тип функції, що є єдиним параметром конструктора агента (і статичного методу Start) 24:
static member Start: (MailboxProcessor <'Msg>--> Async <unit>)--> MailboxProcessor <' Msg>
З цього визначення видно, що основною «робочою конячкою» агента є функція, на вхід отримує примірник самого агента і повертає асинхронну операцію, про які йшлося трохи вище.
Природно, що пряме відповідність агентів і потоков25 було б не дуже зручно і вкрай неефективно, тому що сильно обмежувало б кількість одночасно працюючих агентів. Завдяки використанню асинхронних потоків операцій, агент більшу частину часу є просто структурою в пам'яті, яка містить деякий внутрішній стан і тільки в ті моменти, коли в чергу надходить чергове повідомлення, функція обробки реєструється для виконання в потоці з системного пулу. Функцією обробки якраз і є та, що була передана в конструктор або метод Start. Таким чином, весь внутрішній стан агента підтримується інфраструктурою, а не лягає тяжким вантажем на плечі користувача. Для підтвердження цього факту можна спробувати створити кілька тисяч агентів, запустити їх і почати випадковим чином відправляти їм повідомлення.
Обробка подій
Спочатку. NET дозволяє обробляти події по одному. Обробником події є функція, яка викликається щоразу з деякими аргументами, і якщо розробнику необхідно зберігати якесь додаткове стан між викликами подій — це доводиться робити самостійно. Крім того, оригінальна модель підписки може призводити до витоку пам'яті через наявність неявних взаємних посилань у передплатника і генераторі подій.
F #, звичайно, дозволяє працювати з подіями в класичному розумінні. Правда, робиться це за допомогою трохи незвичайного синтаксису: замість використання операторів + = і -= для реєстрації і деактивації обробника події використовується пара методів Add / Remove.
button.Click.Add (fun args--> printfn «Button clicked»)
З іншого боку, F # дозволяє маніпулювати потоками подій, і працювати з ними як з послідовностями, використовуючи функції filter, map, split та інші. Наприклад, наступний код фільтрує потік подій натискання клавіш всередині поля введення, вибираючи лише ті з них, які були натиснуті в поєднанні з Ctrl, після чого, з усіх аргументів подій вибираються тільки значення поля KeyCode. Таким чином, значенням keyCodes буде потік подій, які містять лише коди клавіш, натиснутих з дзвінком Ctrl.
> Event.map (fun args--> args.KeyCode)
Варто відзначити, що обробка потоків подій дозволяє розробнику не піклуватися про тонкощі відписки, а просто генерувати нові потоки подій на основі вже існуючих, використовувати ці потоки як значення, тобто передавати їх як аргументи та повертати з функцій.
Використання подібної техніки може призвести до значного спрощення коду для реалізації, наприклад, функціональності Drag & Drop. Адже це є ні що інше, як композиція події натискання кнопки миші, переміщення курсору з натиснутою кнопкою і потім відпускання. Для прикладу частину, що відповідає за drag, можна легко перекласти з російської на F #:
> Event.merge form.MouseMove
Поєднання обробки потоків подій з асинхронними потоками операцій дозволяє також досить просто вирішувати сумно відому проблему GUI-додатків, коли обробник події графічного компоненту повинен виконуватись в основному потоці і будь-яке зволікання призведе до «зависання» інтерфейсу додатку.
Приклади використання
Існує цікавий приклад використання quotations в F# як засобу мета-програмування. Завдання пов'язана з масивної паралельною обробкою даних за допомогою спеціальної бібліотеки або на багатоядерній процесорній системі x64, або на графічному процесорі GPU. Визначається невелика підмножина F#, код з якої може бути відтранслюваним і запущеним на цільовій платформі. Це дозволяє писати «звичайні» функції на F#, налагоджувати їх стандартним чином, а потім за допомогою механізму quotations обробляти ці функції і запускати на GPU. Більш того, програма на F# може створювати «на льоту» такі функції, які вже потім можуть відтранслюватися і запуститися на іншій платформі. Примітно, що при реалізації такої трансляції широко використовується спеціальна можливість F # у вигляді активного зіставлення зі зразком (active pattern matching), яка помітно спрощує написання транслятора.
Мова F# може бути зручна для створення DSL, які стають частиною самого F#, причому такі мови можуть бути досить короткими і виразними. Наприклад, у книзі Real-World Functional Programming наводиться бібліотека для опису анімації. Первісна ідея була реалізована в проекті Fran [Elliot, Hudak, 1997] на мові Haskell. Опис ідеї ще можна знайти в книзі The Haskell School of Expression [6]. Анімація моделюється як залежна від часу величина. На основі примітивів будуються вже складові лексикон предметної області функції, за допомогою яких можна описувати досить складні анімації і робити це декларативно. Що стосується реалізації, то там багато спільного з монадами.
Ось приклад того, як виглядає на мові F# визначення анімованої частини сонячної системи, яку потім можна візуалізувати на екрані комп'ютера:
Наступним прикладом використання F# є комерційний продукт WebSharper фірми IntelliFactory [2]. Це платформа для створення клієнтських web-додатків. Вона дозволяє писати клієнтський код на F#, який потім буде відтранслювати на JavaScript. Така трансляція поширюється на достатню велику підмножину F #, включаючи функціональне ядро мови, алгебричні типи даних, класи, об'єкти, винятки й делегати. Також підтримується значна частина стандартної бібліотеки F #, включаючи роботу з послідовностями (sequences), подіями і асинхронними обчислювальними виразами (async workflows). Все може бути автоматично відтранслювати на цільову мову JavaScript. Крім того, підтримується деяка частина стандартних класів самого. NET, і обсяг підтримки буде зростати.
Цей продукт примітний тим, що тут використовується цілий ряд прийомів, характерних для функціонального програмування. Так, в WebSharper вбудований DSL для завдання HTML-коду, в якому широко застосовуються комбінатори.
Для самої ж трансляції в JavaScript використовуються цитування. Код для трансляції повинен бути помічений атрибутом JavaScriptAttribute, успадкованим від стандартного ReflectedDefinitionAttribute.