type Item =
{Title: string;
DueDate: DateTime;}
Full name: index.Item
Item.Title: string
Multiple items
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = String
Full name: Microsoft.FSharp.Core.string
Item.DueDate: DateTime
Multiple items
type DateTime =
struct
new : ticks:int64 -> DateTime + 10 overloads
member Add : value:TimeSpan -> DateTime
member AddDays : value:float -> DateTime
member AddHours : value:float -> DateTime
member AddMilliseconds : value:float -> DateTime
member AddMinutes : value:float -> DateTime
member AddMonths : months:int -> DateTime
member AddSeconds : value:float -> DateTime
member AddTicks : value:int64 -> DateTime
member AddYears : value:int -> DateTime
...
end
Full name: System.DateTime
--------------------
DateTime()
(+0 other overloads)
DateTime(ticks: int64) : unit
(+0 other overloads)
DateTime(ticks: int64, kind: DateTimeKind) : unit
(+0 other overloads)
DateTime(year: int, month: int, day: int) : unit
(+0 other overloads)
DateTime(year: int, month: int, day: int, calendar: Globalization.Calendar) : unit
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : unit
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: DateTimeKind) : unit
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: Globalization.Calendar) : unit
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int) : unit
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, kind: DateTimeKind) : unit
(+0 other overloads)
type Model = Item list
Full name: index.Model
type 'T list = List<'T>
Full name: Microsoft.FSharp.Collections.list<_>
type Message =
| Add of Item
| Reset
Full name: index.Message
union case Message.Add: Item -> Message
union case Message.Reset: Message
type unit = Unit
Full name: Microsoft.FSharp.Core.unit
Multiple items
module Event
from Microsoft.FSharp.Control
--------------------
type Event<'T> =
new : unit -> Event<'T>
member Trigger : arg:'T -> unit
member Publish : IEvent<'T>
Full name: Microsoft.FSharp.Control.Event<_>
--------------------
type Event<'Delegate,'Args (requires delegate and 'Delegate :> Delegate)> =
new : unit -> Event<'Delegate,'Args>
member Trigger : sender:obj * args:'Args -> unit
member Publish : IEvent<'Delegate,'Args>
Full name: Microsoft.FSharp.Control.Event<_,_>
--------------------
new : unit -> Event<'T>
--------------------
new : unit -> Event<'Delegate,'Args>
union case Option.None: Option<'T>
val id : x:'T -> 'T
Full name: Microsoft.FSharp.Core.Operators.id
union case Option.Some: Value: 'T -> Option<'T>
Wrattler
My experience with TypeScript
Wrattler
New notebook system for data science
Interactive, smart dependency tracking and caching
Client-side in TyepScript!
What Wrattler does
Next generation of data science notebooks
Polyglot - mix multuple languages
Reproducible - dependency tracking
Interactive - do more in the browser
Smart - support for AI assistants
Building Wrattler
Why we choose TypeScript
- I'm an F# person!
- The team knew TypeScript
- Jupyter uses TypeScript
Reflections on TypeScript
- The Good: Decent for functional programming!
- The Bad: Difficult trade-offs in type system
- The Ugly: You'd think it has simple syntax
The Good
Functional programming with TypeScript
Elm architecture
Wrattler code structure
- Immutable Model and Event
- Pure update and render
- Almost no dependencies
Why not just use React
- Not very fancy user interface
- Avoid fast-paced JavaScript world
- Control over how things work
- Core React is pretty simple!
DEMO
Elm architecture in F#
Domain modelling
Modelling TODO list with F# types
1:
2:
3:
4:
5:
|
type Item = { Title : string; DueDate : DateTime }
type Model = list<Item>
type Message =
| Add of Item
| Reset
|
Elm-architecture operations
1:
2:
|
val update : Model -> Message -> Model
val render : (Message -> unit) -> Model -> Html
|
Functional TypeScript
Strict mode is great!
- strictNullChecks to avoid null
- noImplicitAny to avoid any
- strict for more good things
Functional data types
- Interfaces for simple record types
- Unions with singleton types for DUs
- Discipline to avoid mutation :-)
DEMO
Elm architecture in TypeScript
Elmish in TypeScript
Model and discriminated union for events
1:
2:
3:
4:
5:
6:
|
interface Model {
count : number | null
}
interface ResetEvent { kind:"reset" }
interface UpdateEvent { kind:"update", by:number }
type Event = ResetEvent | UpdateEvent
|
Elm-architecture operations
1:
2:
|
render : (trigger:((event:TEvent) => void), state:TState) => VNode
update : (state:TState, event:TEvent) => TState
|
The Bad
Trade-offs in the type system
Extensibility
External language
Kernel with eval
Language plugin
Block with custom user interface
AI assistants
Clever component
Language plugins
Custom user interface, evaluation, dependencies
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
|
interface LanguagePlugin {
language : string
editor : Editor<EditorState, any>
parse(code:string) : Block
evaluate(context:EvaluationContext, node:Graph.Node)
: Promise<EvaluationResult>
bind(context:BindingContext, block: Block)
: Promise<BindingResult>
save(block:Block):string;
}
|
DEMO
Implementing language plugins
Classes vs. Interfaces (1/5)
When you might want a class
- If you want object-oriented style
- If you want checks using instanceof
When you do not want a class
- If you want somewhat lighter syntax
- If you want somewhat more flexibility
Classes vs. Interfaces (2/5)
Anonymous interface implementations are great!
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
|
const myLangPlugin : Langs.LanguagePlugin = {
language: "merger",
iconClassName: "fa fa-object-group",
editor: mergerEditor,
getDefaultCode: (id:number) => "",
parse: (code:string) : MergerBlock => {
let [outName, inputs] = code.split('=')
return { language: "merger",
output: outName, inputs: inputs.split(',') }
}
}
|
Classes vs. Interfaces (3/5)
But switching to a class is pain
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
|
class MyLangPlugin implements Langs.LanguagePlugin {
language = "merger"
iconClassName = "fa fa-object-group"
editor = mergerEditor
getDefaultCode = (id:number) => ""
parse(code:string) : MergerBlock {
let [outName, inputs] = code.split('=')
return { language: "merger",
output: outName, inputs: inputs.split(',') }
}
}
|
Classes vs. Interfaces (4/5)
Writing constructors is so much work!
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
|
export class JsLanguagePlugin implements LanguagePlugin {
readonly language: string
readonly editor: Langs.Editor<JsState, JsEvent>
readonly datastoreURI:string
constructor(datastoreURI:string) {
this.language = "javascript"
this.editor= javascriptEditor
this.datastoreURI = datastoreURI
}
parse (code:string): JavascriptBlockKind{
return new JavascriptBlockKind(code);
}
}
|
Classes vs. Interfaces (5/5)
Can I get F# implicit class syntax please?
1:
2:
3:
4:
5:
6:
7:
8:
|
type JsLanguagePlugin(datastoreURI:string)
let language = "javascript"
let editor= javascriptEditor
let datastoreURI = datastoreURI
interface LanguagePlugin with
member x.parse(code:string) =
JavascriptBlockKind(code)
|
Breaking the type system (1/3)
You cannot fully avoid the any type
1:
2:
3:
4:
5:
6:
7:
|
let response = await axios.get(url, {headers:{Inputs:header}});
let data = response.data;
return data.map(r => ({
name: r.name,
path: r.path.split("/").filter((p:string) => p != "")
}));
|
F# type providers are nice, but don't always help
You can get more checks with noImplicitAny
Breaking the type system (2/3)
It is still JavaScript underneath...
1:
2:
3:
4:
5:
6:
7:
8:
|
interface Variable {
name : string
}
type Scope = { [variable:string]: Variable }
function getVarLength(scope:Scope, k:string) : number {
return scope[k] != undefined ? scope[k].name.length : -1;
}
|
Can getVarLength fail because name is undefined?
Breaking the type system (3/3)
It is still JavaScript underneath...
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
|
interface Variable {
name : string
}
type Scope = { [variable:string]: Variable }
function getVarLength(scope:Scope, k:string) : number {
return scope[k] != undefined ? scope[k].name.length : -1;
}
let evil = getVarLength({}, "toString") // Boom!
|
Poor type inference (1/3)
F# can typically infer types from the first use
1:
2:
3:
4:
5:
|
let mutable outputs = []
let addOutput = fun f ->
outputs <- f::outputs
evaluateCode code addOutput
|
Poor type inference (2/3)
Type inference in the strict mode is poor
1:
2:
3:
4:
5:
|
var outputs = [];
var addOutput = function(f) {
outputs.push(f)
}
evaluateCode(code, addOutput)
|
Poor type inference (3/3)
Type inference in the strict mode is poor
1:
2:
3:
4:
5:
|
var outputs : ((id:string) => void)[] = [];
var addOutput = function(f:(id:string) => void) {
outputs.push(f)
}
evaluateCode(code, addOutput)
|
The Ugly
Syntax for functional programming
AI assistants
Tools for semi-automated data wrangling
New code block with custom user interface
DEMO
Wrattler AI assistants
DEMO
Implementing AI assistant code block
Two function syntaxes (1/2)
Function definition using a lambda
1:
2:
3:
4:
5:
6:
7:
8:
|
let createAiaEditor = (assistants:AiAssistant[])
: Langs.Editor<AiaState, AiaEvent> => ({
initialize: (id:number, block:Langs.Block) => {
let aiaBlock = <AiaBlock>block
return { id: id, block: aiaBlock, chain: [],
expanded: false, assistants: assistants }
}
});
|
Two function syntaxes (2/2)
Explicit function using a keyword
1:
2:
3:
4:
5:
6:
7:
8:
|
function createAiaEditor(assistants:AiAssistant[])
: Langs.Editor<AiaState, AiaEvent> {
return { initialize: (id:number, block:Langs.Block) => {
let aiaBlock = <AiaBlock>block
return { id: id, block: aiaBlock, chain: [],
expanded: false, assistants: assistants }
} };
};
|
Generating HTML (1/2)
Building HTML nodes is hard work!
1:
2:
3:
4:
5:
6:
7:
8:
9:
|
let previewButton =
h('button', { onclick:() =>
ctx.evaluate(cell.editor.id) }, ["Evaluate!"] )
let spinner =
h('i', {class: 'fa fa-spinner fa-spin' }, [])
let preview = h('div', {class:'preview'}, [
(cell.code.value == undefined) ?
(cell.evaluationState == 'pending') ? spinner : previewButton :
(createOutputPreview(cell, cell.code.value))]);
|
Generating HTML (2/2)
F# comprehensions are so nice!
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
|
let preview = h?div ["class"=>"preview"] [
match cell.code.value, cell.evaluationState with
| None, "pending" ->
yield h?i ["class" => "fa fa-spinner" ] []
| None, _ ->
yield h?button [ "onclick" =!> fun _ _ ->
ctx.evaluate cell.editor.id ] [ text "Evaluate!" ]
| Some value, _ ->
yield createOutputPreview cell value
]
|
Conclusions
TypeScript for F# Zealots
TypeScript for F# Zealots
Lessons learned from Wrattler project
Can support functional patterns
Works fine with Elm architecture
Not designed for functional style
Trying to be JavaScript or not?
F# for the web
Functional core
Safe and clean
Extra integration
Makes trade-offs
|
TypeScript
Many styles
OOP, functional
Does not hide JS
Cloudy mix of all
|
TypeScript for F# Zealots
TypeScript experience
Elmish architecture works!
Hard to be disciplined
Project context
Team, community, app type
Would I choose TypeScript again?
Tomas Petricek, University of Kent and fsharpWorks
tomas@tomasp.net | @tomaspetricek