1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096 |
- <!doctype html><!-- This is a valid HTML5 document. -->
- <!-- Screen readers, SEO, extensions and so on. -->
- <html lang=fr>
- <!-- Has to be within the first 1024 bytes, hence before the <title>
- See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
- <meta charset=utf-8>
- <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
- <!-- The viewport meta is quite crowded and we are responsible for that.
- See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
- <meta name=viewport content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no">
- <!-- Required to make a valid HTML5 document. -->
- <title>A practical introduction to functional programming (archive) — David Larlet</title>
- <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
- <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons/apple-touch-icon.png">
- <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons/favicon-32x32.png">
- <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons/favicon-16x16.png">
- <link rel="manifest" href="/manifest.json">
- <link rel="mask-icon" href="/static/david/icons/safari-pinned-tab.svg" color="#5bbad5">
- <link rel="shortcut icon" href="/static/david/icons/favicon.ico">
- <meta name="apple-mobile-web-app-title" content="David Larlet">
- <meta name="application-name" content="David Larlet">
- <meta name="msapplication-TileColor" content="#da532c">
- <meta name="msapplication-config" content="/static/david/icons/browserconfig.xml">
- <meta name="theme-color" content="#f0f0ea">
- <!-- That good ol' feed, subscribe :p. -->
- <link rel=alternate type="application/atom+xml" title=Feed href="/david/log/">
-
- <meta name="robots" content="noindex, nofollow">
- <meta content="origin-when-cross-origin" name="referrer">
- <!-- Canonical URL for SEO purposes -->
- <link rel="canonical" href="http://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming">
-
- <style>
- /* http://meyerweb.com/eric/tools/css/reset/ */
- html, body, div, span,
- h1, h2, h3, h4, h5, h6, p, blockquote, pre,
- a, abbr, address, big, cite, code,
- del, dfn, em, img, ins,
- small, strike, strong, tt, var,
- dl, dt, dd, ol, ul, li,
- fieldset, form, label, legend,
- table, caption, tbody, tfoot, thead, tr, th, td,
- article, aside, canvas, details, embed,
- figure, figcaption, footer, header, hgroup,
- menu, nav, output, ruby, section, summary,
- time, mark, audio, video {
- margin: 0;
- padding: 0;
- border: 0;
- font-size: 100%;
- font: inherit;
- vertical-align: baseline;
- }
- /* HTML5 display-role reset for older browsers */
- article, aside, details, figcaption, figure,
- footer, header, hgroup, menu, nav, section { display: block; }
- body { line-height: 1; }
- blockquote, q { quotes: none; }
- blockquote:before, blockquote:after,
- q:before, q:after {
- content: '';
- content: none;
- }
- table {
- border-collapse: collapse;
- border-spacing: 0;
- }
-
- /* http://practicaltypography.com/equity.html */
- /* https://calendar.perfplanet.com/2016/no-font-face-bulletproof-syntax/ */
- /* https://www.filamentgroup.com/lab/js-web-fonts.html */
- @font-face {
- font-family: 'EquityTextB';
- src: url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff2') format('woff2'),
- url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff') format('woff');
- font-weight: 300;
- font-style: normal;
- font-display: swap;
- }
- @font-face {
- font-family: 'EquityTextB';
- src: url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff2') format('woff2'),
- url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff') format('woff');
- font-weight: 300;
- font-style: italic;
- font-display: swap;
- }
- @font-face {
- font-family: 'EquityTextB';
- src: url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff2') format('woff2'),
- url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff') format('woff');
- font-weight: 700;
- font-style: normal;
- font-display: swap;
- }
-
- @font-face {
- font-family: 'ConcourseT3';
- src: url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff2') format('woff2'),
- url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff') format('woff');
- font-weight: 300;
- font-style: normal;
- font-display: swap;
- }
-
-
- /* http://practice.typekit.com/lesson/caring-about-opentype-features/ */
- body {
- /* http://www.cssfontstack.com/ Palatino 99% Win 86% Mac */
- font-family: "EquityTextB", Palatino, serif;
- background-color: #f0f0ea;
- color: #07486c;
- font-kerning: normal;
- -moz-osx-font-smoothing: grayscale;
- -webkit-font-smoothing: subpixel-antialiased;
- text-rendering: optimizeLegibility;
- font-variant-ligatures: common-ligatures contextual;
- font-feature-settings: "kern", "liga", "clig", "calt";
- }
- pre, code, kbd, samp, var, tt {
- font-family: 'TriplicateT4c', monospace;
- }
- em {
- font-style: italic;
- color: #323a45;
- }
- strong {
- font-weight: bold;
- color: black;
- }
- nav {
- background-color: #323a45;
- color: #f0f0ea;
- display: flex;
- justify-content: space-around;
- padding: 1rem .5rem;
- }
- nav:last-child {
- border-bottom: 1vh solid #2d7474;
- }
- nav a {
- color: #f0f0ea;
- }
- nav abbr {
- border-bottom: 1px dotted white;
- }
-
- h1 {
- border-top: 1vh solid #2d7474;
- border-bottom: .2vh dotted #2d7474;
- background-color: #e3e1e1;
- color: #323a45;
- text-align: center;
- padding: 5rem 0 4rem 0;
- width: 100%;
- font-family: 'ConcourseT3';
- display: flex;
- flex-direction: column;
- }
- h1.single {
- padding-bottom: 10rem;
- }
- h1 span {
- position: absolute;
- top: 1vh;
- left: 20%;
- line-height: 0;
- }
- h1 span a {
- line-height: 1.7;
- padding: 1rem 1.2rem .6rem 1.2rem;
- border-radius: 0 0 6% 6%;
- background: #2d7474;
- font-size: 1.3rem;
- color: white;
- text-decoration: none;
- }
- h2 {
- margin: 4rem 0 1rem;
- border-top: .2vh solid #2d7474;
- padding-top: 1vh;
- }
- h3 {
- text-align: center;
- margin: 3rem 0 .75em;
- }
- hr {
- height: .4rem;
- width: .4rem;
- border-radius: .4rem;
- background: #07486c;
- margin: 2.5rem auto;
- }
- time {
- display: bloc;
- margin-left: 0 !important;
- }
- ul, ol {
- margin: 2rem;
- }
- ul {
- list-style-type: square;
- }
- a {
- text-decoration-skip-ink: auto;
- text-decoration-thickness: 0.05em;
- text-underline-offset: 0.09em;
- }
- article {
- max-width: 50rem;
- display: flex;
- flex-direction: column;
- margin: 2rem auto;
- }
- article.single {
- border-top: .2vh dotted #2d7474;
- margin: -6rem auto 1rem auto;
- background: #f0f0ea;
- padding: 2rem;
- }
- article p:last-child {
- margin-bottom: 1rem;
- }
- p {
- padding: 0 .5rem;
- margin-left: 3rem;
- }
- p + p,
- figure + p {
- margin-top: 2rem;
- }
-
- blockquote {
- background-color: #e3e1e1;
- border-left: .5vw solid #2d7474;
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 1rem;
- margin: 1.5rem;
- }
- blockquote cite {
- font-style: italic;
- }
- blockquote p {
- margin-left: 0;
- }
-
- figure {
- border-top: .2vh solid #2d7474;
- background-color: #e3e1e1;
- text-align: center;
- padding: 1.5rem 0;
- margin: 1rem 0 0;
- font-size: 1.5rem;
- width: 100%;
- }
- figure img {
- max-width: 250px;
- max-height: 250px;
- border: .5vw solid #323a45;
- padding: 1px;
- }
- figcaption {
- padding: 1rem;
- line-height: 1.4;
- }
- aside {
- display: flex;
- flex-direction: column;
- background-color: #e3e1e1;
- padding: 1rem 0;
- border-bottom: .2vh solid #07486c;
- }
- aside p {
- max-width: 50rem;
- margin: 0 auto;
- }
-
- /* https://fvsch.com/code/css-locks/ */
- p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
- font-size: 1rem;
- line-height: calc( 1.5em + 0.2 * 1rem );
- }
- h1 {
- font-size: 1.9rem;
- line-height: calc( 1.2em + 0.2 * 1rem );
- }
- h2 {
- font-size: 1.6rem;
- line-height: calc( 1.3em + 0.2 * 1rem );
- }
- h3 {
- font-size: 1.35rem;
- line-height: calc( 1.4em + 0.2 * 1rem );
- }
- @media (min-width: 20em) {
- /* The (100vw - 20rem) / (50 - 20) part
- resolves to 0-1rem, depending on the
- viewport width (between 20em and 50em). */
- p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
- font-size: calc( 1rem + .6 * (100vw - 20rem) / (50 - 20) );
- line-height: calc( 1.5em + 0.2 * (100vw - 50rem) / (20 - 50) );
- margin-left: 0;
- }
- h1 {
- font-size: calc( 1.9rem + 1.5 * (100vw - 20rem) / (50 - 20) );
- line-height: calc( 1.2em + 0.2 * (100vw - 50rem) / (20 - 50) );
- }
- h2 {
- font-size: calc( 1.5rem + 1.5 * (100vw - 20rem) / (50 - 20) );
- line-height: calc( 1.3em + 0.2 * (100vw - 50rem) / (20 - 50) );
- }
- h3 {
- font-size: calc( 1.35rem + 1.5 * (100vw - 20rem) / (50 - 20) );
- line-height: calc( 1.4em + 0.2 * (100vw - 50rem) / (20 - 50) );
- }
- }
- @media (min-width: 50em) {
- /* The right part of the addition *must* be a
- rem value. In this example we *could* change
- the whole declaration to font-size:2.5rem,
- but if our baseline value was not expressed
- in rem we would have to use calc. */
- p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
- font-size: calc( 1rem + .6 * 1rem );
- line-height: 1.5em;
- }
- p, li, pre, details {
- margin-left: 3rem;
- }
- h1 {
- font-size: calc( 1.9rem + 1.5 * 1rem );
- line-height: 1.2em;
- }
- h2 {
- font-size: calc( 1.5rem + 1.5 * 1rem );
- line-height: 1.3em;
- }
- h3 {
- font-size: calc( 1.35rem + 1.5 * 1rem );
- line-height: 1.4em;
- }
- figure img {
- max-width: 500px;
- max-height: 500px;
- }
- }
-
- figure.unsquared {
- margin-bottom: 1.5rem;
- }
- figure.unsquared img {
- height: inherit;
- }
-
-
-
- @media print {
- body { font-size: 100%; }
- a:after { content: " (" attr(href) ")"; }
- a, a:link, a:visited, a:after {
- text-decoration: underline;
- text-shadow: none !important;
- background-image: none !important;
- background: white;
- color: black;
- }
- abbr[title] { border-bottom: 0; }
- abbr[title]:after { content: " (" attr(title) ")"; }
- img { page-break-inside: avoid; }
- @page { margin: 2cm .5cm; }
- h1, h2, h3 { page-break-after: avoid; }
- p3 { orphans: 3; widows: 3; }
- img {
- max-width: 250px !important;
- max-height: 250px !important;
- }
- nav, aside { display: none; }
- }
-
- ul.with_columns {
- column-count: 1;
- }
- @media (min-width: 20em) {
- ul.with_columns {
- column-count: 2;
- }
- }
- @media (min-width: 50em) {
- ul.with_columns {
- column-count: 3;
- }
- }
- ul.with_two_columns {
- column-count: 1;
- }
- @media (min-width: 20em) {
- ul.with_two_columns {
- column-count: 1;
- }
- }
- @media (min-width: 50em) {
- ul.with_two_columns {
- column-count: 2;
- }
- }
-
- .gallery {
- display: flex;
- flex-wrap: wrap;
- justify-content: space-around;
- }
- .gallery figure img {
- margin-left: 1rem;
- margin-right: 1rem;
- }
- .gallery figure figcaption {
- font-family: 'ConcourseT3'
- }
-
- footer {
- font-family: 'ConcourseT3';
- display: flex;
- flex-direction: column;
- border-top: 3px solid white;
- padding: 4rem 0;
- background-color: #07486c;
- color: white;
- }
- footer > * {
- max-width: 50rem;
- margin: 0 auto;
- }
- footer a {
- color: #f1c40f;
- }
- footer .avatar {
- width: 200px;
- height: 200px;
- border-radius: 50%;
- float: left;
- -webkit-shape-outside: circle();
- shape-outside: circle();
- margin-right: 2rem;
- padding: 2px 5px 5px 2px;
- background: white;
- border-left: 1px solid #f1c40f;
- border-top: 1px solid #f1c40f;
- border-right: 5px solid #f1c40f;
- border-bottom: 5px solid #f1c40f;
- }
- </style>
-
- <h1>
- <span><a id="jumper" href="#jumpto" title="Un peu perdu ?">?</a></span>
- A practical introduction to functional programming (archive)
- <time>Pour la pérennité des contenus liés. Non-indexé, retrait sur simple email.</time>
- </h1>
- <section>
- <article>
- <h3><a href="http://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming">Source originale du contenu</a></h3>
- <p>Many functional programming articles teach abstract functional techniques. That is, composition, pipelining, higher order functions. This one is different. It shows examples of imperative, unfunctional code that people write every day and translates these examples to a functional style.</p>
-
- <p>The first section of the article takes short, data transforming loops and translates them into functional maps and reduces. The second section takes longer loops, breaks them up into units and makes each unit functional. The third section takes a loop that is a long series of successive data transformations and decomposes it into a functional pipeline.</p>
-
- <p>The examples are in Python, because many people find Python easy to read. A number of the examples eschew pythonicity in order to demonstrate functional techniques common to many languages: map, reduce, pipeline.</p>
-
- <h3>A guide rope</h3>
-
- <p>When people talk about functional programming, they mention a dizzying number of “functional” characteristics. They mention immutable data<sup>1</sup>, first class functions<sup>2</sup> and tail call optimisation<sup>3</sup>. These are language features that aid functional programming. They mention mapping, reducing, pipelining, recursing, currying<sup>4</sup> and the use of higher order functions. These are programming techniques used to write functional code. They mention parallelization<sup>5</sup>, lazy evaluation<sup>6</sup> and determinism<sup>7</sup>. These are advantageous properties of functional programs.</p>
-
- <p>Ignore all that. Functional code is characterised by one thing: the absence of side effects. It doesn’t rely on data outside the current function, and it doesn’t change data that exists outside the current function. Every other “functional” thing can be derived from this property. Use it as a guide rope as you learn.</p>
-
- <p>This is an unfunctional function:</p>
-
- <pre class="prettyprint">
- a = 0
- def increment1():
- global a
- a += 1
- </pre>
-
- <p>This is a functional function:</p>
-
- <pre class="prettyprint">
- def increment2(a):
- return a + 1
- </pre>
-
- <h3>Don’t iterate over lists. Use map and reduce.</h3>
-
- <h4>Map</h4>
-
- <p>Map takes a function and a collection of items. It makes a new, empty collection, runs the function on each item in the original collection and inserts each return value into the new collection. It returns the new collection.</p>
-
- <p>This is a simple map that takes a list of names and returns a list of the lengths of those names:</p>
-
- <pre class="prettyprint">
- name_lengths = map(len, ["Mary", "Isla", "Sam"])
-
- print name_lengths
- # => [4, 4, 3]
- </pre>
-
- <p>This is a map that squares every number in the passed collection:</p>
-
- <pre class="prettyprint">
- squares = map(lambda x: x * x, [0, 1, 2, 3, 4])
-
- print squares
- # => [0, 1, 4, 9, 16]
- </pre>
-
- <p>This map doesn’t take a named function. It takes an anonymous, inlined function defined with <code>lambda</code>. The parameters of the lambda are defined to the left of the colon. The function body is defined to the right of the colon. The result of running the function body is (implicitly) returned.</p>
-
- <p>The unfunctional code below takes a list of real names and replaces them with randomly assigned code names.</p>
-
- <pre class="prettyprint">
- import random
-
- names = ['Mary', 'Isla', 'Sam']
- code_names = ['Mr. Pink', 'Mr. Orange', 'Mr. Blonde']
-
- for i in range(len(names)):
- names[i] = random.choice(code_names)
-
- print names
- # => ['Mr. Blonde', 'Mr. Blonde', 'Mr. Blonde']
- </pre>
-
- <p>(As you can see, this algorithm can potentially assign the same secret code name to multiple secret agents. Hopefully, this won’t be a source of confusion during the secret mission.)</p>
-
- <p>This can be rewritten as a map:</p>
-
- <pre class="prettyprint">
- import random
-
- names = ['Mary', 'Isla', 'Sam']
-
- secret_names = map(lambda x: random.choice(['Mr. Pink',
- 'Mr. Orange',
- 'Mr. Blonde']),
- names)
- </pre>
-
- <p><strong>Exercise 1</strong>. Try rewriting the code below as a map. It takes a list of real names and replaces them with code names produced using a more robust strategy.</p>
-
- <pre class="prettyprint">
- names = ['Mary', 'Isla', 'Sam']
-
- for i in range(len(names)):
- names[i] = hash(names[i])
-
- print names
- # => [6306819796133686941, 8135353348168144921, -1228887169324443034]
- </pre>
-
- <p>(Hopefully, the secret agents will have good memories and won’t forget each other’s secret code names during the secret mission.)</p>
-
- <p>My solution:</p>
-
- <pre class="prettyprint">
- names = ['Mary', 'Isla', 'Sam']
-
- secret_names = map(hash, names)
- </pre>
-
- <h4>Reduce</h4>
-
- <p>Reduce takes a function and a collection of items. It returns a value that is created by combining the items.</p>
-
- <p>This is a simple reduce. It returns the sum of all the items in the collection.</p>
-
- <pre class="prettyprint">
- sum = reduce(lambda a, x: a + x, [0, 1, 2, 3, 4])
-
- print sum
- # => 10
- </pre>
-
- <p><code>x</code> is the current item being iterated over. <code>a</code> is the accumulator. It is the value returned by the execution of the lambda on the previous item. <code>reduce()</code> walks through the items. For each one, it runs the lambda on the current <code>a</code> and <code>x</code> and returns the result as the <code>a</code> of the next iteration.</p>
-
- <p>What is <code>a</code> in the first iteration? There is no previous iteration result for it to pass along. <code>reduce()</code> uses the first item in the collection for <code>a</code> in the first iteration and starts iterating at the second item. That is, the first <code>x</code> is the second item.</p>
-
- <p>This code counts how often the word <code>'Sam'</code> appears in a list of strings:</p>
-
- <pre class="prettyprint">
- sentences = ['Mary read a story to Sam and Isla.',
- 'Isla cuddled Sam.',
- 'Sam chortled.']
-
- sam_count = 0
- for sentence in sentences:
- sam_count += sentence.count('Sam')
-
- print sam_count
- # => 3
- </pre>
-
- <p>This is the same code written as a reduce:</p>
-
- <pre class="prettyprint">
- sentences = ['Mary read a story to Sam and Isla.',
- 'Isla cuddled Sam.',
- 'Sam chortled.']
-
- sam_count = reduce(lambda a, x: a + x.count('Sam'),
- sentences,
- 0)
- </pre>
-
- <p>How does this code come up with its initial <code>a</code>? The starting point for the number of incidences of <code>'Sam'</code> cannot be <code>'Mary read a story to Sam and Isla.'</code> The initial accumulator is specified with the third argument to <code>reduce()</code>. This allows the use of a value of a different type from the items in the collection.</p>
-
- <p>Why are map and reduce better?</p>
-
- <p>First, they are often one-liners.</p>
-
- <p>Second, the important parts of the iteration – the collection, the operation and the return value – are always in the same places in every map and reduce.</p>
-
- <p>Third, the code in a loop may affect variables defined before it or code that runs after it. By convention, maps and reduces are functional.</p>
-
- <p>Fourth, map and reduce are elemental operations. Every time a person reads a <code>for</code> loop, they have to work through the logic line by line. There are few structural regularities they can use to create a scaffolding on which to hang their understanding of the code. In contrast, map and reduce are at once building blocks that can be combined into complex algorithms, and elements that the code reader can instantly understand and abstract in their mind. “Ah, this code is transforming each item in this collection. It’s throwing some of the transformations away. It’s combining the remainder into a single output.”</p>
-
- <p>Fifth, map and reduce have many friends that provide useful, tweaked versions of their basic behaviour. For example: <code>filter</code>, <code>all</code>, <code>any</code> and <code>find</code>.</p>
-
- <p><strong>Exercise 2</strong>. Try rewriting the code below using map, reduce and filter. Filter takes a function and a collection. It returns a collection of every item for which the function returned <code>True</code>.</p>
-
- <pre class="prettyprint">
- people = [{'name': 'Mary', 'height': 160},
- {'name': 'Isla', 'height': 80},
- {'name': 'Sam'}]
-
- height_total = 0
- height_count = 0
- for person in people:
- if 'height' in person:
- height_total += person['height']
- height_count += 1
-
- if height_count > 0:
- average_height = height_total / height_count
-
- print average_height
- # => 120
- </pre>
-
- <p>If this seems tricky, try not thinking about the operations on the data. Think of the states the data will go through, from the list of people dictionaries to the average height. Don’t try and bundle multiple transformations together. Put each on a separate line and assign the result to a descriptively-named variable. Once the code works, condense it.</p>
-
- <p>My solution:</p>
-
- <pre class="prettyprint">
- people = [{'name': 'Mary', 'height': 160},
- {'name': 'Isla', 'height': 80},
- {'name': 'Sam'}]
-
- heights = map(lambda x: x['height'],
- filter(lambda x: 'height' in x, people))
-
- if len(heights) > 0:
- from operator import add
- average_height = reduce(add, heights) / len(heights)
- </pre>
-
- <h3>Write declaratively, not imperatively</h3>
-
- <p>The program below runs a race between three cars. At each time step, each car may move forwards or it may stall. At each time step, the program prints out the paths of the cars so far. After five time steps, the race is over.</p>
-
- <p>This is some sample output:</p>
-
- <pre class="prettyprint">
- -
- --
- --
-
- --
- --
- ---
-
- ---
- --
- ---
-
- ----
- ---
- ----
-
- ----
- ----
- -----
- </pre>
-
- <p>This is the program:</p>
-
- <pre class="prettyprint">
- from random import random
-
- time = 5
- car_positions = [1, 1, 1]
-
- while time:
- # decrease time
- time -= 1
-
- print ''
- for i in range(len(car_positions)):
- # move car
- if random() > 0.3:
- car_positions[i] += 1
-
- # draw car
- print '-' * car_positions[i]
- </pre>
-
- <p>The code is written imperatively. A functional version would be declarative. It would describe what to do, rather than how to do it.</p>
-
- <h4>Use functions</h4>
-
- <p>A program can be made more declarative by bundling pieces of the code into functions.</p>
-
- <pre class="prettyprint">
- from random import random
-
- def move_cars():
- for i, _ in enumerate(car_positions):
- if random() > 0.3:
- car_positions[i] += 1
-
- def draw_car(car_position):
- print '-' * car_position
-
- def run_step_of_race():
- global time
- time -= 1
- move_cars()
-
- def draw():
- print ''
- for car_position in car_positions:
- draw_car(car_position)
-
- time = 5
- car_positions = [1, 1, 1]
-
- while time:
- run_step_of_race()
- draw()
- </pre>
-
- <p>To understand this program, the reader just reads the main loop. “If there is time left, run a step of the race and draw. Check the time again.” If the reader wants to understand more about what it means to run a step of the race, or draw, they can read the code in those functions.</p>
-
- <p>There are no comments any more. The code describes itself.</p>
-
- <p>Splitting code into functions is a great, low brain power way to make code more readable.</p>
-
- <p>This technique uses functions, but it uses them as sub-routines. They parcel up code. The code is not functional in the sense of the guide rope. The functions in the code use state that was not passed as arguments. They affect the code around them by changing external variables, rather than by returning values. To check what a function really does, the reader must read each line carefully. If they find an external variable, they must find its origin. They must see what other functions change that variable.</p>
-
- <h4>Remove state</h4>
-
- <p>This is a functional version of the car race code:</p>
-
- <pre class="prettyprint">
- from random import random
-
- def move_cars(car_positions):
- return map(lambda x: x + 1 if random() > 0.3 else x,
- car_positions)
-
- def output_car(car_position):
- return '-' * car_position
-
- def run_step_of_race(state):
- return {'time': state['time'] - 1,
- 'car_positions': move_cars(state['car_positions'])}
-
- def draw(state):
- print ''
- print '\n'.join(map(output_car, state['car_positions']))
-
- def race(state):
- draw(state)
- if state['time']:
- race(run_step_of_race(state))
-
- race({'time': 5,
- 'car_positions': [1, 1, 1]})
- </pre>
-
- <p>The code is still split into functions, but the functions are functional. There are three signs of this. First, there are no longer any shared variables. <code>time</code> and <code>car_positions</code> get passed straight into <code>race()</code>. Second, functions take parameters. Third, no variables are instantiated inside functions. All data changes are done with return values. <code>race()</code> recurses<sup>3</sup> with the result of <code>run_step_of_race()</code>. Each time a step generates a new state, it is passed immediately into the next step.</p>
-
- <p>Now, here are two functions, <code>zero()</code> and <code>one()</code>:</p>
-
- <pre class="prettyprint">
- def zero(s):
- if s[0] == "0":
- return s[1:]
-
- def one(s):
- if s[0] == "1":
- return s[1:]
- </pre>
-
- <p><code>zero()</code> takes a string, <code>s</code>. If the first character is <code>'0'</code>, it returns the rest of the string. If it is not, it returns <code>None</code>, the default return value of Python functions. <code>one()</code> does the same, but for a first character of <code>'1'</code>.</p>
-
- <p>Imagine a function called <code>rule_sequence()</code>. It takes a string and a list of rule functions of the form of <code>zero()</code> and <code>one()</code>. It calls the first rule on the string. Unless <code>None</code> is returned, it takes the return value and calls the second rule on it. Unless <code>None</code> is returned, it takes the return value and calls the third rule on it. And so forth. If any rule returns <code>None</code>, <code>rule_sequence()</code> stops and returns <code>None</code>. Otherwise, it returns the return value of the final rule.</p>
-
- <p>This is some sample input and output:</p>
-
- <pre class="prettyprint">
- print rule_sequence('0101', [zero, one, zero])
- # => 1
-
- print rule_sequence('0101', [zero, zero])
- # => None
- </pre>
-
- <p>This is the imperative version of <code>rule_sequence()</code>:</p>
-
- <pre class="prettyprint">
- def rule_sequence(s, rules):
- for rule in rules:
- s = rule(s)
- if s == None:
- break
-
- return s
- </pre>
-
- <p><strong>Exercise 3</strong>. The code above uses a loop to do its work. Make it more declarative by rewriting it as a recursion.</p>
-
- <p>My solution:</p>
-
- <pre class="prettyprint">
- def rule_sequence(s, rules):
- if s == None or not rules:
- return s
- else:
- return rule_sequence(rules[0](s), rules[1:])
- </pre>
-
- <h3>Use pipelines</h3>
-
- <p>In the previous section, some imperative loops were rewritten as recursions that called out to auxiliary functions. In this section, a different type of imperative loop will be rewritten using a technique called a pipeline.</p>
-
- <p>The loop below performs transformations on dictionaries that hold the name, incorrect country of origin and active status of some bands.</p>
-
- <pre class="prettyprint">
- bands = [{'name': 'sunset rubdown', 'country': 'UK', 'active': False},
- {'name': 'women', 'country': 'Germany', 'active': False},
- {'name': 'a silver mt. zion', 'country': 'Spain', 'active': True}]
-
- def format_bands(bands):
- for band in bands:
- band['country'] = 'Canada'
- band['name'] = band['name'].replace('.', '')
- band['name'] = band['name'].title()
-
- format_bands(bands)
-
- print bands
- # => [{'name': 'Sunset Rubdown', 'active': False, 'country': 'Canada'},
- # {'name': 'Women', 'active': False, 'country': 'Canada' },
- # {'name': 'A Silver Mt Zion', 'active': True, 'country': 'Canada'}]
- </pre>
-
- <p>Worries are stirred by the name of the function. “format” is very vague. Upon closer inspection of the code, these worries begin to claw. Three things happen in the same loop. The <code>'country'</code> key gets set to <code>'Canada'</code>. Punctuation is removed from the band name. The band name gets capitalized. It is hard to tell what the code is intended to do and hard to tell if it does what it appears to do. The code is hard to reuse, hard to test and hard to parallelize.</p>
-
- <p>Compare it with this:</p>
-
- <pre class="prettyprint">
- print pipeline_each(bands, [set_canada_as_country,
- strip_punctuation_from_name,
- capitalize_names])
- </pre>
-
- <p>This code is easy to understand. It gives the impression that the auxiliary functions are functional because they seem to be chained together. The output from the previous one comprises the input to the next. If they are functional, they are easy to verify. They are also easy to reuse, easy to test and easy to parallelize.</p>
-
- <p>The job of <code>pipeline_each()</code> is to pass the bands, one at a time, to a transformation function, like <code>set_canada_as_country()</code>. After the function has been applied to all the bands, <code>pipeline_each()</code> bundles up the transformed bands. Then, it passes each one to the next function.</p>
-
- <p>Let’s look at the transformation functions.</p>
-
- <pre class="prettyprint">
- def assoc(_d, key, value):
- from copy import deepcopy
- d = deepcopy(_d)
- d[key] = value
- return d
-
- def set_canada_as_country(band):
- return assoc(band, 'country', "Canada")
-
- def strip_punctuation_from_name(band):
- return assoc(band, 'name', band['name'].replace('.', ''))
-
- def capitalize_names(band):
- return assoc(band, 'name', band['name'].title())
- </pre>
-
- <p>Each one associates a key on a band with a new value. There is no easy way to do this without mutating the original band. <code>assoc()</code> solves this problem by using <code>deepcopy()</code> to produce a copy of the passed dictionary. Each transformation function makes its modification to the copy and returns that copy.</p>
-
- <p>Everything seems fine. Band dictionary originals are protected from mutation when a key is associated with a new value. But there are two other potential mutations in the code above. In <code>strip_punctuation_from_name()</code>, the unpunctuated name is generated by calling <code>replace()</code> on the original name. In <code>capitalize_names()</code>, the capitalized name is generated by calling <code>title()</code> on the original name. If <code>replace()</code> and <code>title()</code> are not functional, <code>strip_punctuation_from_name()</code> and <code>capitalize_names()</code> are not functional.</p>
-
- <p>Fortunately, <code>replace()</code> and <code>title()</code> do not mutate the strings they operate on. This is because strings are immutable in Python. When, for example, <code>replace()</code> operates on a band name string, the original band name is copied and <code>replace()</code> is called on the copy. Phew.</p>
-
- <p>This contrast between the mutability of strings and dictionaries in Python illustrates the appeal of languages like Clojure. The programmer need never think about whether they are mutating data. They aren’t.</p>
-
- <p><strong>Exercise 4</strong>. Try and write the <code>pipeline_each</code> function. Think about the order of operations. The bands in the array are passed, one band at a time, to the first transformation function. The bands in the resulting array are passed, one band at a time, to the second transformation function. And so forth.</p>
-
- <p>My solution:</p>
-
- <pre class="prettyprint">
- def pipeline_each(data, fns):
- return reduce(lambda a, x: map(x, a),
- fns,
- data)
- </pre>
-
- <p>All three transformation functions boil down to making a change to a particular field on the passed band. <code>call()</code> can be used to abstract that. It takes a function to apply and the key of the value to apply it to.</p>
-
- <pre class="prettyprint">
- set_canada_as_country = call(lambda x: 'Canada', 'country')
- strip_punctuation_from_name = call(lambda x: x.replace('.', ''), 'name')
- capitalize_names = call(str.title, 'name')
-
- print pipeline_each(bands, [set_canada_as_country,
- strip_punctuation_from_name,
- capitalize_names])
- </pre>
-
- <p>Or, if we are willing to sacrifice readability for conciseness, just:</p>
-
- <pre class="prettyprint">
- print pipeline_each(bands, [call(lambda x: 'Canada', 'country'),
- call(lambda x: x.replace('.', ''), 'name'),
- call(str.title, 'name')])
- </pre>
-
- <p>The code for <code>call()</code>:</p>
-
- <pre class="prettyprint">
- def assoc(_d, key, value):
- from copy import deepcopy
- d = deepcopy(_d)
- d[key] = value
- return d
-
- def call(fn, key):
- def apply_fn(record):
- return assoc(record, key, fn(record.get(key)))
- return apply_fn
- </pre>
-
- <p>There is a lot going on here. Let’s take it piece by piece.</p>
-
- <p>One. <code>call()</code> is a higher order function. A higher order function takes a function as an argument, or returns a function. Or, like <code>call()</code>, it does both.</p>
-
- <p>Two. <code>apply_fn()</code> looks very similar to the three transformation functions. It takes a record (a band). It looks up the value at <code>record[key]</code>. It calls <code>fn</code> on that value. It assigns the result back to a copy of the record. It returns the copy.</p>
-
- <p>Three. <code>call()</code> does not do any actual work. <code>apply_fn()</code>, when called, will do the work. In the example of using <code>pipeline_each()</code> above, one instance of <code>apply_fn()</code> will set <code>'country'</code> to <code>'Canada'</code> on a passed band. Another instance will capitalize the name of a passed band.</p>
-
- <p>Four. When an <code>apply_fn()</code> instance is run, <code>fn</code> and <code>key</code> will not be in scope. They are neither arguments to <code>apply_fn()</code>, nor locals inside it. But they will still be accessible. When a function is defined, it saves references to the variables it closes over: those that were defined in a scope outside the function and that are used inside the function. When the function is run and its code references a variable, Python looks up the variable in the locals and in the arguments. If it doesn’t find it there, it looks in the saved references to closed over variables. This is where it will find <code>fn</code> and <code>key</code>.</p>
-
- <p>Five. There is no mention of bands in the <code>call()</code> code. That is because <code>call()</code> could be used to generate pipeline functions for any program, regardless of topic. Functional programming is partly about building up a library of generic, reusable, composable functions.</p>
-
- <p>Good job. Closures, higher order functions and variable scope all covered in the space of a few paragraphs. Have a nice glass of lemonade.</p>
-
- <p>There is one more piece of band processing to do. That is to remove everything but the name and country. <code>extract_name_and_country()</code> can pull that information out:</p>
-
- <pre class="prettyprint">
- def extract_name_and_country(band):
- plucked_band = {}
- plucked_band['name'] = band['name']
- plucked_band['country'] = band['country']
- return plucked_band
-
- print pipeline_each(bands, [call(lambda x: 'Canada', 'country'),
- call(lambda x: x.replace('.', ''), 'name'),
- call(str.title, 'name'),
- extract_name_and_country])
-
- # => [{'name': 'Sunset Rubdown', 'country': 'Canada'},
- # {'name': 'Women', 'country': 'Canada'},
- # {'name': 'A Silver Mt Zion', 'country': 'Canada'}]
- </pre>
-
- <p><code>extract_name_and_country()</code> could have been written as a generic function called <code>pluck()</code>. <code>pluck()</code> would be used like this:</p>
-
- <pre class="prettyprint">
- print pipeline_each(bands, [call(lambda x: 'Canada', 'country'),
- call(lambda x: x.replace('.', ''), 'name'),
- call(str.title, 'name'),
- pluck(['name', 'country'])])
- </pre>
-
- <p><strong>Exercise 5</strong>. <code>pluck()</code> takes a list of keys to extract from each record. Try and write it. It will need to be a higher order function.</p>
-
- <p>My solution:</p>
-
- <pre class="prettyprint">
- def pluck(keys):
- def pluck_fn(record):
- return reduce(lambda a, x: assoc(a, x, record[x]),
- keys,
- {})
- return pluck_fn
- </pre>
-
- <h3>What now?</h3>
-
- <p>Functional code co-exists very well with code written in other styles. The transformations in this article can be applied to any code base in any language. Try applying them to your own code.</p>
-
- <p>Think of Mary, Isla and Sam. Turn iterations of lists into maps and reduces.</p>
-
- <p>Think of the race. Break code into functions. Make those functions functional. Turn a loop that repeats a process into a recursion.</p>
-
- <p>Think of the bands. Turn a sequence of operations into a pipeline.</p>
-
- <p><br/></p>
-
- <p><sup>1</sup> An immutable piece of data is one that cannot be changed. Some languages, like Clojure, make all values immutable by default. Any “mutating” operations copy the value, change it and pass back the changed copy. This eliminates bugs that arise from a programmer’s incomplete model of the possible states their program may enter.</p>
-
- <p><sup>2</sup> Languages that support first class functions allow functions to be treated like any other value. This means they can be created, passed to functions, returned from functions and stored inside data structures.</p>
-
- <p><sup>3</sup> Tail call optimisation is a programming language feature. Each time a function recurses, a new stack frame is created. A stack frame is used to store the arguments and local values for the current function invocation. If a function recurses a large number of times, it is possible for the interpreter or compiler to run out of memory. Languages with tail call optimisation reuse the same stack frame for their entire sequence of recursive calls. Languages like Python that do not have tail call optimisation generally limit the number of times a function may recurse to some number in the thousands. In the case of the <code>race()</code> function, there are only five time steps, so it is safe.</p>
-
- <p><sup>4</sup> Currying means decomposing a function that takes multiple arguments into a function that takes the first argument and returns a function that takes the next argument, and so forth for all the arguments.</p>
-
- <p><sup>5</sup> Parallelization means running the same code concurrently without synchronization. These concurrent processes are often run on multiple processors.</p>
-
- <p><sup>6</sup> Lazy evaluation is a compiler technique that avoids running code until the result is needed.</p>
-
- <p><sup>7</sup> A process is deterministic if repetitions yield the same result every time.</p>
- </article>
- </section>
-
-
- <nav id="jumpto">
- <p>
- <a href="/david/blog/">Accueil du blog</a> |
- <a href="http://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming">Source originale</a> |
- <a href="/david/stream/2019/">Accueil du flux</a>
- </p>
- </nav>
-
- <footer>
- <div>
- <img src="/static/david/david-larlet-avatar.jpg" loading="lazy" class="avatar" width="200" height="200">
- <p>
- Bonjour/Hi!
- Je suis <a href="/david/" title="Profil public">David Larlet</a>, je vis actuellement à Montréal et j’alimente cet espace depuis 15 ans. <br>
- Si tu as apprécié cette lecture, n’hésite pas à poursuivre ton exploration. Par exemple via les <a href="/david/blog/" title="Expériences bienveillantes">réflexions bimestrielles</a>, la <a href="/david/stream/2019/" title="Pensées (dés)articulées">veille hebdomadaire</a> ou en t’abonnant au <a href="/david/log/" title="S’abonner aux publications via RSS">flux RSS</a> (<a href="/david/blog/2019/flux-rss/" title="Tiens c’est quoi un flux RSS ?">so 2005</a>).
- </p>
- <p>
- Je m’intéresse à la place que je peux avoir dans ce monde. En tant qu’humain, en tant que membre d’une famille et en tant qu’associé d’une coopérative. De temps en temps, je fais aussi des <a href="https://github.com/davidbgk" title="Principalement sur Github mais aussi ailleurs">trucs techniques</a>. Et encore plus rarement, <a href="/david/talks/" title="En ce moment je laisse plutôt la place aux autres">j’en parle</a>.
- </p>
-
- <p>
- Voici quelques articles choisis :
- <a href="/david/blog/2019/faire-equipe/" title="Accéder à l’article complet">Faire équipe</a>,
- <a href="/david/blog/2018/bivouac-automnal/" title="Accéder à l’article complet">Bivouac automnal</a>,
- <a href="/david/blog/2018/commodite-effondrement/" title="Accéder à l’article complet">Commodité et effondrement</a>,
- <a href="/david/blog/2017/donnees-communs/" title="Accéder à l’article complet">Des données aux communs</a>,
- <a href="/david/blog/2016/accompagner-enfant/" title="Accéder à l’article complet">Accompagner un enfant</a>,
- <a href="/david/blog/2016/senior-developer/" title="Accéder à l’article complet">Senior developer</a>,
- <a href="/david/blog/2016/illusion-sociale/" title="Accéder à l’article complet">L’illusion sociale</a>,
- <a href="/david/blog/2016/instantane-scopyleft/" title="Accéder à l’article complet">Instantané Scopyleft</a>,
- <a href="/david/blog/2016/enseigner-web/" title="Accéder à l’article complet">Enseigner le Web</a>,
- <a href="/david/blog/2016/simplicite-defaut/" title="Accéder à l’article complet">Simplicité par défaut</a>,
- <a href="/david/blog/2016/minimalisme-esthetique/" title="Accéder à l’article complet">Minimalisme et esthétique</a>,
- <a href="/david/blog/2014/un-web-omni-present/" title="Accéder à l’article complet">Un web omni-présent</a>,
- <a href="/david/blog/2014/manifeste-developpeur/" title="Accéder à l’article complet">Manifeste de développeur</a>,
- <a href="/david/blog/2013/confort-convivialite/" title="Accéder à l’article complet">Confort et convivialité</a>,
- <a href="/david/blog/2013/testament-numerique/" title="Accéder à l’article complet">Testament numérique</a>,
- et <a href="/david/blog/" title="Accéder aux archives">bien d’autres…</a>
- </p>
- <p>
- On peut <a href="mailto:david%40larlet.fr" title="Envoyer un courriel">échanger par courriel</a>. Si éventuellement tu souhaites que l’on travaille ensemble, tu devrais commencer par consulter le <a href="http://larlet.com">profil dédié à mon activité professionnelle</a> et/ou contacter directement <a href="http://scopyleft.fr/">scopyleft</a>, la <abbr title="Société coopérative et participative">SCOP</abbr> dont je fais partie depuis six ans. Je recommande au préalable de lire <a href="/david/blog/2018/cout-site/" title="Attention ce qui va suivre peut vous choquer">combien coûte un site</a> et pourquoi je suis plutôt favorable à une <a href="/david/pro/devis/" title="Discutons-en !">non-demande de devis</a>.
- </p>
- <p>
- Je ne traque pas ta navigation mais mon
- <abbr title="Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33.184162340">hébergeur</abbr>
- conserve des logs d’accès.
- </p>
- </div>
- </footer>
- <script type="text/javascript">
- ;(_ => {
- const jumper = document.getElementById('jumper')
- jumper.addEventListener('click', e => {
- e.preventDefault()
- const anchor = e.target.getAttribute('href')
- const targetEl = document.getElementById(anchor.substring(1))
- targetEl.scrollIntoView({behavior: 'smooth'})
- })
- })()
- </script>
|