Upload
maxim-avanov
View
177
Download
2
Tags:
Embed Size (px)
Citation preview
Declarative Programming&
Algebraic Data Types *
Maxim Avanovmaximavanov.com
* from Django's perspective
19th Moscow Django Meetup
Our Goal1. «Outsource» boilerplate (i.e. concentrate on important).2. Check as much as possible, and as soon as possible.3. Component coherence.
1. A story of How & WhatConcentrate on important
How vs. Whatdef handle_article_form(request): if request.method == 'POST': form = ArticleForm(request.POST) if form.is_valid(): save_article(form.cleaned_data) return HttpResponseRedirect('/success/') else: form = ArticleForm()
return render(request, 'article_form.html', {'form': form})
Howif request.method == 'POST': form = ArticleForm(request.POST) if form.is_valid(): # ...else: # ...
What# case 1save_article(form.cleaned_data)return HttpResponseRedirect('/success/')
# case 2form = ArticleForm()return render(request, 'article_form.html', {'form': form})
A few things to worry aboutdef handle_article_form(request): if request.method == 'POST': form = ArticleForm(request.POST) if form.is_valid(): save_article(form.cleaned_data) return HttpResponseRedirect('/success/') else: form = ArticleForm()
return render(request, 'article_form.html', {'form': form})
«What» is obscured by «How»redundant detailsSingle Responsibility principle is violated
If we could get rid of How's once and for all,would we miss them?
Constraint programming
Tribute to Pyramidfrom rhetoric import view_config, view_defaults
@view_defaults(route_name='articles', renderer='article_form.html')class ArticlesHandler(object): def __init__(self, request): self.request = request
@view_config(request_method='GET') def article_form(self): form = ArticleForm() return {'form': form}
@view_config(request_method='POST', validate_form=ArticleForm) def save_article(self): save_article(self.request.form.cleaned_data) return HttpResponseRedirect('/success/')
@view_config(request_method='POST') def invalid_article_form(self): return {'form': self.request.form}
What it actually means# We have an Articles Handler: ArticlesHandler# It renders a template: article_form.html# A user shall be able to add new entries: article_form()# If we submit valid ArticleForm: save_article()# If we submit invalid ArticleForm: invalid_article_form()
class ArticlesHandler(object): def __init__(self, request): self.request = request
def article_form(self): form = ArticleForm() return {'form': form}
def save_article(self): save_article(self.request.form.cleaned_data) return HttpResponseRedirect('/success/')
def invalid_article_form(self): return {'form': self.request.form}
...or in other words---view: ArticlesHandlerGET: article_formPOST: - validate: articles.ArticleForm view: save_article - view: invalid_article_form
class ArticlesHandler(object): def __init__(self, request): self.request = request
def article_form(self): form = ArticleForm() return {'form': form}
def save_article(self): save_article(self.request.form.cleaned_data) return HttpResponseRedirect('/success/')
def invalid_article_form(self): return {'form': self.request.form}
A couple of other examples
Different ways to do the same thing@view_defaults(route_name='authentication', renderer='auth_form.html')class AuthenticationHandler(object):
@view_config(request_method='POST', validate_form=EmailAuthForm) def auth_with_email(self): # ...
@view_config(request_method='POST', validate_form=SMSAuthForm) def auth_with_sms(self): # ...
@view_config(request_method='POST', validate_form=LoginAuthForm) def auth_with_login(self): # ...
@view_config(request_method='POST') def on_invalid_form(self): # ...
API versioningconfig.add_route('api.workflows', '/api/workflows')
@view_defaults(route_name='api.workflows', api_version='<2.0')class WorkflowsAPIv1(object): # ...
@view_defaults(route_name='api.workflows', api_version='>=2.0')class WorkflowsAPIv2(object): # ...
@view_defaults(route_name='api.workflows', renderer='json')class WorkflowsAPI(object):
@view_config(request_method='POST', api_version='<2.0') def create_new_workflow_v1(self): # ...
@view_config(request_method='POST', api_version='>=2.0') def create_new_workflow_v2(self): # ...
Algebraic Data Types
OCaml ADT exampleWatch a «Caml Trading» talk by Yaron Minsky at
http://youtu.be/hKcOkWzj0_s?t=31m6s
OCaml ADT exampletype order = { id: int; price: float; size: int; }type cancel = { xid: int; }
type instruction = | Order of order | Cancel of cancel
let filter_by_oid instructions oid = List.filter instructions (fun x -> match x with | Order o -> o.id = oid | Cancel c -> c.xid = oid)
OCaml ADT exampletype order = { id: int; price: float; size: int; }type cancel = { xid: int; }
type cancel_replace = { xr_id: int; new_price: float; new_size: int; }
type instruction = | Order of order | Cancel of cancel | Cancel_replace of cancel_replace
let filter_by_oid instructions oid = List.filter instructions (fun x -> match x with | Order o -> o.id = oid | Cancel c -> c.xid = oid)
Warning P: This pattern-matching is not exhaustiveHere is an example of a value that is not matched...
2. Trying to reproduceCheck as much as possible, and as soon as possible
Python ADT *
* kind of
Python ADT example
Models (product types)
from django.db import models
class Order(models.Model): tid = models.IntegerField() price = models.DecimalField(max_digits=16, decimal_places=4) size = models.IntegerField()
class Cancel(models.Model): xtid = models.IntegerField()
class CancelReplace(models.Model): xr_tid = models.IntegerField() new_price = models.DecimalField(max_digits=16, decimal_places=4) new_size = models.IntegerField()
Python ADT example
Smart Enums (union types)
from rhetoric.adt import adtfrom .models import Order, Cancel, CancelReplace
class Instruction(adt): ORDER = Order CANCEL = Cancel CANCEL_REPLACE = CancelReplace
Python ADT example
Cases (match statement)
from .types import Instruction
@Instruction.ORDER('filter_by_oid')def filter_order_by_oid(order, oid): return order.tid == oid
@Instruction.CANCEL('filter_by_oid')def filter_cancel_by_oid(cancel, oid): return cancel.xtid == oid
@Instruction.CANCEL_REPLACE('filter_by_oid')def filter_cancel_replace_by_oid(cr, oid): return cr.xr_tid == oid
def filter_by_oid(instructions, oid): return list(filter( lambda i: Instruction.match(i)['filter_by_oid'](i, oid), instructions))
Python ADT example
Cases (2)
from .types import Instruction
inline_matcher = Instruction.inline_match( ORDER = (lambda o, oid: o.tid == oid), CANCEL = (lambda c, oid: c.xtid == oid), CANCEL_REPLACE = (lambda cr, oid: cr.xr_tid == oid))
def filter_by_oid_alt(instructions, oid): return list(filter(lambda i: inline_matcher(i)(i, oid), instructions))
3. Use case: multilingual contentComponent coherence
Use case
Define ADT
from rhetoric.adt import adt
class Language(adt): ENGLISH = 'en' GERMAN = 'de'
Use case
Register Models
from django.db import modelsfrom .types import Language
class IRegionalArticle(models.Model): class Meta: abstract = True title = models.CharField(max_length=140, default='') text = models.TextField(max_length=65536, default='')
@Language.ENGLISH('db:articles')class EnglishArticle(IRegionalArticle): pass
@Language.GERMAN('db:articles')class GermanArticle(IRegionalArticle): pass
Use case
Process requests
from rhetoric.view import view_config, view_defaultsfrom .types import Language
@view_defaults(route_name='articles.regional.index', renderer='json')class ArticlesHandler(object): def __init__(self, request, language): self.request = request self.language = language self.language_strategy = Language.match(language)
@view_config(request_method='GET') def show_local_entries(self): return {'ok': True}
Consistency check
Adding a new language
class Language(adt): ENGLISH = 'en' GERMAN = 'de' SPANISH = 'es'
ConfigurationError: Case db:articles of <class 'project.articles.types.Language'> is not exhaustive. Here is the variant that is not matched: SPANISH
# We have to register this [email protected]('db:articles')class SpanishArticle(IRegionalArticle): pass
Consistency check
Using undefined variant
from rhetoric.adt import adt
class Language(adt): ENGLISH = 'en' GERMAN = 'de'
inline_matcher = Language.inline_match( ENGLISH = lambda: EnglishArticle.objects.all(), GERMAN = lambda: GermanArticle.objects.all(), SPANISH = lambda: SpanishArticle.objects.all())
PatternError: Variant SPANISH does not belong to the type <class 'project.articles.types.Language'>
Consistency check
Guard boundaries
from .types import Language
def includeme(config): RULES = {'language': Language} config.add_route('articles.regional.index', '/articles/{language}', RULES)
/articles/{language} => ̂articles/(?P<language>(?:de|en))$
class Language(adt): ENGLISH = 'en' GERMAN = 'de' SPANISH = 'es'
/articles/{language} => ̂articles/(?P<language>(?:de|en|es))$
Use case
Strategy map
class ArticlesHandler(object): def __init__(self, request, language): self.request = request self.language = language self.language_strategy = Language.match(language)
GET /articles/de{'db:articles': <class 'project.articles.models.GermanArticle'>}
GET /articles/en{'db:articles': <class 'project.articles.models.EnglishArticle'>}
Beyond today's topicThings worth mentioning
github.com/lihaoyi/macropygithub.com/benanhalt/PyAlgebraicDataTypes (cons ADT)«Say What You Mean» talk by Ryan Kelly
Thank you
Q & A
Credits talk by Yaron Minsky«Caml Trading»
Rhetoric ProjectVenusian Project