Funktionale Programmierung mit JavaScript

λ
1. April: Michael Häuslmann
15. April: Marinus Noichl

Quellen & Source- und Beispielcode

Github Repository

https://github.com/mihaeu/afp-js
Travis Build License Node v5.9.1
Professortocat

Agenda

  • Geschichte & Herkunft
  • Sprachgrundlagen & Eigenschaften
  • Funktionale Konzepte
  • Anwendungsbereiche
  • Beispiele

Dauer: ~45min (exkl. Fragen)

Geschichte & Herkunft


Wer ist an der Hochschule bereits mit JavaScript in Berührung gekommen?


Wer arbeitet privat/beruflich mit JavaScript?


Wer meint JavaScript ist eine „schöne“ Sprache?

Geschichte & Herkunft



[...] you have the power to define your own subset. You can write better programs by relying exclusively on the good parts. - Douglas Crockford, JavaScript - The Good Parts

Geschichte & Herkunft

  • 1995 von Netscape entwickelt (in 10 Tagen) für deren Browser
  • verschiedene Implementierungen u.a. von Microsoft (JScript)
  • Sprachstandard erst später entstanden (ECMAScript)
  • aktueller Standard ist ES6 und wird von den meisten modernen Browsern zum Teil implementiert
  • viele Runtime Engines, Cross-Compiler und Super-Sets
    (GWT, TypeScript, Dart, Coffeescript, ClojureScript...)
Google GWT

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.Window;

public class HelloWorld implements EntryPoint {
    public void onModuleLoad() {
        Window.alert("Hello, World!");
    }
}
    
CoffeeScript

Python + Ruby + Haskell =


# Conditions:
number = -42 if opposite

# Functions:
square = (x) -> x * x

# Splats:
race = (winner, runners...) ->
print winner, runners

# Array comprehensions:
cubes = (math.cube num for num in list)
    
TypeScript

class Greeter {
    constructor(public greeting: string) { }

    greet() {
        return "

" + this.greeting + "

"; } }; var greeter = new Greeter("Hello, world!"); document.body.innerHTML = greeter.greet();

Geschichte & Herkunft

  • Keine (ernsthaften) Alternativen
  • früher außschließlich im Frontend, jetzt überall
    (Backend, Desktop- und Mobileapps)
  • NodeJS als Java Umgebung fürs Backend
    (mit V8 von Google als Runtime)
  • unsere Meinung: sehr viel Hype, aber auch echte Chancen

Agenda

  • Geschichte & Herkunft
    • kommt aus dem Web
    • jetzt überall
    • viele Altlasten
    • sehr aktive und beliebt
  • Funktionale Konzepte
  • Anwendungsbereiche
  • Beispiele

Sprachgrundlagen & Eigenschaften


var schlafEin = (anzahlSchafe) => {
  var i = 0, ausgabe = [];
  for (i = anzahlSchafe; i > 0; --i) {
    if (i === 1) {
      ausgabe.push(1 + " Schaf");
    } else {
      ausgabe.push(i + " Schafe");
    }
  }
  return ausgabe.join("\n") + "\nZzzz ...";
};
schlafEin(5);

    

Sprachgrundlagen & Eigenschaften

Datenstrukturen


// boolean
true
false

// number
1
3.1415
(3.1415).toString()

// string
'Hello'

// regex
/java[sS]cript/
    
Alles ist ein Objekt (auch Funktionen selbst)!

Sprachgrundlagen & Eigenschaften

Datenstrukturen


// assoziatives Array (functioncal scope)
var arr = [1, 2, 3];

// Objekte ähnlich wie JSON (global scope)
obj = {
    bezeichner: 'wert'
}

// seit ES6: Map und Set
// immutable Map (block scope)
const map = new Map([[ 1, 'one' ]]);

// Set (block scope)
let set = new Set([1, 1, 1, 2]); // Set { 1, 2 }
    

Sprachgrundlagen & Eigenschaften

  • Interpretiert
  • Schwache dynamische Typisierung
  • Type Coercion
  • Lexikalisches Scoping auf Funktionsebene
  • Prototyp orientiert

Schwache dynamische Typisierung


// praktisch: kein List<Integer> list = new ArrayList<Integer>();
let list = [1, 2, 3, 4, 5];

// dynamisch typisiert, keine Initialisierung notwendig
let organization = 'Microsoft';

// ⚡ neue Variable durch Schreibfehler ⚡
organisation = 'Google';
    

// schwach typisiert
let x = 1;                          // number
x = true;                           // boolean
x = 'true';                         // string
    

Abgesehen für kleine Skripte fast nur Nachteile

Type Coercion


0 == ''           // true

0 == '0'          // true

'' == '0'         // false


false == 'false'  // false

false == '0'      // true

" \t\r\n " == 0   // true
    

Typumwandlungen z.B. bei == != + ...

immer === verwenden!

Lexikalisches Scoping auf Funktionsebene


var x = 3;
function func(randomize) {
    var x;                  // geht nur da functional scope
    if (randomize) {        // bei let error
        let x = Math.random();
        return x;
    }
    return x;
}
func(false);                // undefined
    

immer let verwenden!

Prototyp orientiert

  • Keine Klassen, Methoden, Konstruktoren, Module (zumindest vor ES6)
  • Aber alles über Prototypen möglich

var Animal = (function() {
  function Animal(name) {
    this.name = name;
  }

  Animal.prototype.move = function(meters) {
    return this.name + " moves " + meters + "m.";
  };

  return Animal;
})();

    

Prototyp orientiert

Inheritance :(

function Snake(name, isPoisonous) {
  Animal.call(this, name); // super(name)
  this.isPoisonous = isPoisonous;
}

Snake.prototype = Object.create(Snake.prototype);
Snake.prototype.constructor = Snake;
Snake.prototype.move = function (meters) {
  return this.name + " wiggles " + meters + "m.";
};

    

Prototyp orientiert

(under the hood)

    Seit ES6 viel syntaktischer Zucker:

class Animal {
  constructor(name) {
    this.name = name;
  }
  move(meters) {
    return this.name + " moves " + meters + "m.";
  }
}

class Snake extends Animal {
  move(meters) {
    return this.name + " wiggles " + meters + "m."
  }
}

    

Agenda

  • Geschichte & Herkunft
  • Sprachgrundlagen & Eigenschaften
    • interpretiert & schwach dynamisch typisiert
    • Prototyp-orientiert
    • viele Fallen
    • knappe Schreibweise
  • Anwendungsbereiche
  • Beispiele

Funktionale Konzepte

The good

  • Funktionen waren schon immer ein first-class-citizen
  • seit ES6 Tail Recursion!
  • eingebaute Funktionen höherer Ordnung: filter, reduce, map (aber teils untypische Implementierungen)

The bad

  • keine Lazy Evalution (aber es gibt Libraries)
  • viele Seiteneffekte
  • kein Currying, Pattern Matching, ...

Höhere Funktionen in JavaScript: filter()

Welcher Ansatz ist einfacher zu verstehen und hat weniger mögliche Fehlerquellen?

// imperative
var data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for (var i = 0, result = []; i < data.length; i++) {
  if (data[i] % 2 === 0) {
    result.push(data[i]);
  }
}
return result;


// functional
return [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].filter(i => i % 2 === 0);

    

Höhere Funktionen in JavaScript: map()

Was mache ich, wenn ich eine ähnliche Funktion wie z.B. die Quadratwurzel brauche?

// imperative
data = [0, 1, 2, 3];
for (var i = 0, result = []; i < data.length; i++) {
  result.push(data[i] * data[i]);
}
return result;


// functional
return [0, 1, 2, 3].map(i => i * i);

    

Höhere Funktionen in JavaScript: reduce()


// imperative
let sum = 0;
for (const i of [1, 2, 3, 4]) {   // neues immutable for seit ES6
  sum += i;
}
return sum;


// functional
return [1, 2, 3, 4].reduce((i, j) => i + j);

    

Agenda

  • Geschichte & Herkunft
  • Sprachgrundlagen & Eigenschaften
  • Funktionale Konzepte
    • funktionale Programmierung möglich
    • viel Handarbeit oder Erweiterungen nötig
    • Performanz an manchen Stellen problematisch
  • Beispiele

Anwendungsbereiche

  • Web Frontend, Server Backend, Mobile- & Desktopapps:
    Templating, APIs, DOM, DB Operationen, ...
  • viele Sachen laufen nebenbei
  • JavaScript Umgebungen arbeiten aber mit einem einzigen Thread
  • daher keine Möglichkeit zur Parallelisierung
  • aber JavaScript Umfeld (Web) und Node Architektur Event-basiert

Anwendungsbereiche

Kurzer Ausflug: Node Architektur

The cost of IO

L1-Cache 3 cycles
L2-Cache 14 cycles
RAM 250 cycles
Disk 41 000 000 cycles
Network 240 000 000 cycles

Anwendungsbereiche:

Event Driven Programming

Idee: Langsame externe Events asynchron verarbeiten und weitermachen bis das Ergebnis kommt


Ergebnis: Je nach Anwendungsbereich sehr hoher Durchsatz

Event Loop

Event Loop

Anwendungsbereiche:

Event Driven Programming


fs.readFile('config.js',
    // some time passes...
    function(error, buffer) {
        // the result now pops into existence
        http.get(options, function(resp){
            resp.on('data', function(chunk){
                //do something with chunk
            });
        }).on("error", function(e){
            console.log("Got error: " + e.message);
        });
    }
);
    
Callback Hell Mehr imperativ als deklarativ

Anwendungsbereiche:

Event Driven Programming

Lösung: Futures/Promises (Continuation Monad)

fs.promisifiedReadFile('config.js')
    .then(fetchSomethingFromWeb)
    .then(processThatData)
    .then(saveItToTheDatabase)
    .catch(function(error) { console.log(error); });
    

Anwendungsbereiche:

Event Driven Programming mit Promises

Viele Implementierungen auch mit, fold, forEach, map etc.

fs.promisifiedReadDir("/home/user/workspace")
    .map(fs.promisifiedReadFile)
    .reduce((total, content) => total += content.length, 0)
    .then(result => console.log(result))
    .catch(error => console.log(error));
    

Agenda

  • Geschichte & Herkunft
  • Sprachgrundlagen & Eigenschaften
  • Funktionale Konzepte
  • Anwendungsbereiche
    • Single-Thread Architektur problematisch für komplexe Berechnungen
    • Event-basierte Programmierung gut mit funktionaler Programmierung kombinierbar

Beispiele: Live Coding

  • Sort
  • Currying
  • Funktionen höherer Ordnung

Deckfailcat

Zusammenfassung

  • Geschichte & Herkunft
  • Sprachgrundlagen & Eigenschaften
  • Funktionale Konzepte
  • Anwendungsbereiche
  • Beispiele

Zusammenfassung

JavaScript ist ...
  • ... einfach zu lernen
  • ... überall zu verwenden
  • ... mit vielen Altlasten und Problemen
  • ... für funktionale Programmierung geeignet
  • ... ist aber nicht für rein funktionale Programmierung entworfen

Quellen

Bücher

Blogs

Sonstige Quellen & interessante Links


let add = function(a, b) {
  return a + b;
} 
let add2 = (a, b) => a + b;

let map = (fn, xs) => {
  if (!xs.length) return [];
  return [fn(xs[0])].concat(map(fn, xs.slice(1)));
};

let inc = a => a + 1;
map(inc, [0, 1, 2]);
    

let applyFn = (fn, x) => (y) => fn(x, y);
let inc2 = applyFn(add, 1);

let curry = (fn, ...args) => fn.length === args.length
        ? fn(...args)
        : curry.bind(this, fn, ...args);
let inc3 = curry(add)(1);

    

let sort = xs => {
  if (xs.length === 0) return [];
  let pivot = xs[0], t = xs.slice(1);
  return sort(t.filter(x => x < pivot))
    .concat(pivot)
    .concat(sort(t.filter(x => x >= pivot)));
}