{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Introduction to metaprogramming: \"Code that creates code\" " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Julia has strong **metaprogramming** capabilities. What does this mean?\n", "\n", "> **meta**: something on a higher level\n", "\n", "**metaprogramming** = \"higher-level programming\"\n", "\n", "i.e. writing code (a program) to manipulate not data, but code (that itself manipulates data)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Motivating example: Interact.jl" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Exercise 1\n", "\n", "1. Install the `Interact.jl` package.\n", "\n", "\n", "2. Run the following code" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The square of 1 is 1\n", "The square of 2 is 4\n", "The square of 3 is 9\n", "The square of 4 is 16\n", "The square of 5 is 25\n", "The square of 6 is 36\n", "The square of 7 is 49\n", "The square of 8 is 64\n", "The square of 9 is 81\n", "The square of 10 is 100\n" ] } ], "source": [ "for i in 1:10\n", " j = i^2\n", " println(\"The square of $i is $(j)\")\n", "end" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", " Unable to load WebIO. Please make sure WebIO works for your Jupyter client.\n", " \n", "\n" ], "text/plain": [ "HTML{String}(\"\\n\\n Unable to load WebIO. Please make sure WebIO works for your Jupyter client.\\n \\n\\n\")" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stderr", "output_type": "stream", "text": [ "┌ Warning: Accessing `scope.id` is deprecated, use `scopeid(scope)` instead.\n", "│ caller = ip:0x0\n", "└ @ Core :-1\n" ] }, { "data": { "application/vnd.webio.node+json": { "children": [ { "children": [ { "children": [ { "children": [ { "children": [ { "children": [ "i" ], "instanceArgs": { "namespace": "html", "tag": "label" }, "nodeType": "DOM", "props": { "className": "interact ", "style": { "padding": "5px 10px 0px 10px" } }, "type": "node" } ], "instanceArgs": { "namespace": "html", "tag": "div" }, "nodeType": "DOM", "props": { "className": "interact-flex-row-left" }, "type": "node" }, { "children": [ { "children": [], "instanceArgs": { "namespace": "html", "tag": "input" }, "nodeType": "DOM", "props": { "attributes": { "data-bind": "numericValue: index, valueUpdate: 'input', event: {change: function (){this.changes(this.changes()+1)}}", "orient": "horizontal", "type": "range" }, "className": "slider slider is-fullwidth", "max": 10, "min": 1, "step": 1, "style": {} }, "type": "node" } ], "instanceArgs": { "namespace": "html", "tag": "div" }, "nodeType": "DOM", "props": { "className": "interact-flex-row-center" }, "type": "node" }, { "children": [ { "children": [], "instanceArgs": { "namespace": "html", "tag": "p" }, "nodeType": "DOM", "props": { "attributes": { "data-bind": "text: formatted_val" } }, "type": "node" } ], "instanceArgs": { "namespace": "html", "tag": "div" }, "nodeType": "DOM", "props": { "className": "interact-flex-row-right" }, "type": "node" } ], "instanceArgs": { "namespace": "html", "tag": "div" }, "nodeType": "DOM", "props": { "className": "interact-flex-row interact-widget" }, "type": "node" } ], "instanceArgs": { "handlers": { "changes": [ "(function (val){return (val!=this.model[\"changes\"]()) ? (this.valueFromJulia[\"changes\"]=true, this.model[\"changes\"](val)) : undefined})" ], "index": [ "(function (val){return (val!=this.model[\"index\"]()) ? (this.valueFromJulia[\"index\"]=true, this.model[\"index\"](val)) : undefined})" ] }, "id": "12366578372903537830", "imports": { "data": [ { "name": "knockout", "type": "js", "url": "/assetserver/9a186147d51179d2ec12ccfac1799b0b805a79d5-knockout.js" }, { "name": "knockout_punches", "type": "js", "url": "/assetserver/eccef011301f679ebde0900f01478cac573870ce-knockout_punches.js" }, { "name": null, "type": "js", "url": "/assetserver/302726e4d372a82f7241f522966214f8c37abedd-all.js" }, { "name": null, "type": "css", "url": "/assetserver/2e8309e5997fb0d8cb5d92798adb0f6054224d5e-style.css" }, { "name": null, "type": "css", "url": "/assetserver/b9e7734a025cd12451b49379872363ba19d0f838-bulma_confined.min.css" } ], "type": "async_block" }, "mount_callbacks": [ "function () {\n var handler = (function (ko, koPunches) {\n ko.punches.enableAll();\n ko.bindingHandlers.numericValue = {\n init: function(element, valueAccessor, allBindings, data, context) {\n var stringified = ko.observable(ko.unwrap(valueAccessor()));\n stringified.subscribe(function(value) {\n var val = parseFloat(value);\n if (!isNaN(val)) {\n valueAccessor()(val);\n }\n });\n valueAccessor().subscribe(function(value) {\n var str = JSON.stringify(value);\n if ((str == \"0\") && ([\"-0\", \"-0.\"].indexOf(stringified()) >= 0))\n return;\n if ([\"null\", \"\"].indexOf(str) >= 0)\n return;\n stringified(str);\n });\n ko.applyBindingsToNode(\n element,\n {\n value: stringified,\n valueUpdate: allBindings.get('valueUpdate'),\n },\n context,\n );\n }\n };\n var json_data = {\"formatted_vals\":[\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"10\"],\"changes\":WebIO.getval({\"name\":\"changes\",\"scope\":\"12366578372903537830\",\"id\":\"ob_02\",\"type\":\"observable\"}),\"index\":WebIO.getval({\"name\":\"index\",\"scope\":\"12366578372903537830\",\"id\":\"ob_01\",\"type\":\"observable\"})};\n var self = this;\n function AppViewModel() {\n for (var key in json_data) {\n var el = json_data[key];\n this[key] = Array.isArray(el) ? ko.observableArray(el) : ko.observable(el);\n }\n \n [this[\"formatted_val\"]=ko.computed( function(){\n return this.formatted_vals()[parseInt(this.index())-(1)];\n }\n,this)]\n [this[\"changes\"].subscribe((function (val){!(this.valueFromJulia[\"changes\"]) ? (WebIO.setval({\"name\":\"changes\",\"scope\":\"12366578372903537830\",\"id\":\"ob_02\",\"type\":\"observable\"},val)) : undefined; return this.valueFromJulia[\"changes\"]=false}),self),this[\"index\"].subscribe((function (val){!(this.valueFromJulia[\"index\"]) ? (WebIO.setval({\"name\":\"index\",\"scope\":\"12366578372903537830\",\"id\":\"ob_01\",\"type\":\"observable\"},val)) : undefined; return this.valueFromJulia[\"index\"]=false}),self)]\n \n }\n self.model = new AppViewModel();\n self.valueFromJulia = {};\n for (var key in json_data) {\n self.valueFromJulia[key] = false;\n }\n ko.applyBindings(self.model, self.dom);\n}\n);\n (WebIO.importBlock({\"data\":[{\"name\":\"knockout\",\"type\":\"js\",\"url\":\"/assetserver/9a186147d51179d2ec12ccfac1799b0b805a79d5-knockout.js\"},{\"name\":\"knockout_punches\",\"type\":\"js\",\"url\":\"/assetserver/eccef011301f679ebde0900f01478cac573870ce-knockout_punches.js\"}],\"type\":\"async_block\"})).then((imports) => handler.apply(this, imports));\n}\n" ], "observables": { "changes": { "id": "ob_02", "sync": false, "value": 0 }, "index": { "id": "ob_01", "sync": true, "value": 5 } }, "systemjs_options": null }, "nodeType": "Scope", "props": {}, "type": "node" } ], "instanceArgs": { "namespace": "html", "tag": "div" }, "nodeType": "DOM", "props": { "className": "field interact-widget" }, "type": "node" }, { "children": [ { "children": [], "instanceArgs": { "id": "ob_08", "name": "obs-node" }, "nodeType": "ObservableNode", "props": {}, "type": "node" } ], "instanceArgs": { "handlers": {}, "id": "7743032216781739368", "imports": { "data": [], "type": "async_block" }, "mount_callbacks": [], "observables": { "obs-node": { "id": "ob_08", "sync": false, "value": { "children": [ "The square of 5 is 25" ], "instanceArgs": { "namespace": "html", "tag": "div" }, "nodeType": "DOM", "props": { "className": "interact-flex-row interact-widget" }, "type": "node" } } }, "systemjs_options": null }, "nodeType": "Scope", "props": {}, "type": "node" } ], "instanceArgs": { "namespace": "html", "tag": "div" }, "nodeType": "DOM", "props": {}, "type": "node" }, "text/html": [ "\n", " \n", "\n" ], "text/plain": [ "Node{WebIO.DOM}(WebIO.DOM(:html, :div), Any[Node{DOM}(DOM(:html, :div), Any[Scope(Node{DOM}(DOM(:html, :div), Any[Node{DOM}(DOM(:html, :div), Any[Node{DOM}(DOM(:html, :label), Any[\"i\"], Dict{Symbol,Any}(:className=>\"interact \",:style=>Dict{Any,Any}(:padding=>\"5px 10px 0px 10px\")))], Dict{Symbol,Any}(:className=>\"interact-flex-row-left\")), Node{DOM}(DOM(:html, :div), Any[Node{DOM}(DOM(:html, :input), Any[], Dict{Symbol,Any}(:max=>10,:min=>1,:attributes=>Dict{Any,Any}(:type=>\"range\",Symbol(\"data-bind\")=>\"numericValue: index, valueUpdate: 'input', event: {change: function (){this.changes(this.changes()+1)}}\",\"orient\"=>\"horizontal\"),:step=>1,:className=>\"slider slider is-fullwidth\",:style=>Dict{Any,Any}()))], Dict{Symbol,Any}(:className=>\"interact-flex-row-center\")), Node{DOM}(DOM(:html, :div), Any[Node{DOM}(DOM(:html, :p), Any[], Dict{Symbol,Any}(:attributes=>Dict(\"data-bind\"=>\"text: formatted_val\")))], Dict{Symbol,Any}(:className=>\"interact-flex-row-right\"))], Dict{Symbol,Any}(:className=>\"interact-flex-row interact-widget\")), Dict{String,Tuple{Observables.AbstractObservable,Union{Nothing, Bool}}}(\"changes\"=>(Observable{Int64} with 1 listeners. Value:\n", "0, nothing),\"index\"=>(Observable{Int64} with 2 listeners. Value:\n", "5, nothing)), Set(String[]), nothing, Asset[Asset(\"js\", \"knockout\", \"/Users/dpsanders/.julia/packages/Knockout/1sDlc/src/../assets/knockout.js\"), Asset(\"js\", \"knockout_punches\", \"/Users/dpsanders/.julia/packages/Knockout/1sDlc/src/../assets/knockout_punches.js\"), Asset(\"js\", nothing, \"/Users/dpsanders/.julia/packages/InteractBase/l8cEC/src/../assets/all.js\"), Asset(\"css\", nothing, \"/Users/dpsanders/.julia/packages/InteractBase/l8cEC/src/../assets/style.css\"), Asset(\"css\", nothing, \"/Users/dpsanders/.julia/packages/Interact/0klKX/src/../assets/bulma_confined.min.css\")], Dict{Any,Any}(\"changes\"=>Any[JSString(\"(function (val){return (val!=this.model[\\\"changes\\\"]()) ? (this.valueFromJulia[\\\"changes\\\"]=true, this.model[\\\"changes\\\"](val)) : undefined})\")],\"index\"=>Any[JSString(\"(function (val){return (val!=this.model[\\\"index\\\"]()) ? (this.valueFromJulia[\\\"index\\\"]=true, this.model[\\\"index\\\"](val)) : undefined})\")]), ConnectionPool(Channel{Any}(sz_max:9223372036854775807,sz_curr:0), Set(AbstractConnection[]), Channel{AbstractConnection}(sz_max:32,sz_curr:0)), WebIO.JSString[JSString(\"function () {\\n var handler = (function (ko, koPunches) {\\n ko.punches.enableAll();\\n ko.bindingHandlers.numericValue = {\\n init: function(element, valueAccessor, allBindings, data, context) {\\n var stringified = ko.observable(ko.unwrap(valueAccessor()));\\n stringified.subscribe(function(value) {\\n var val = parseFloat(value);\\n if (!isNaN(val)) {\\n valueAccessor()(val);\\n }\\n });\\n valueAccessor().subscribe(function(value) {\\n var str = JSON.stringify(value);\\n if ((str == \\\"0\\\") && ([\\\"-0\\\", \\\"-0.\\\"].indexOf(stringified()) >= 0))\\n return;\\n if ([\\\"null\\\", \\\"\\\"].indexOf(str) >= 0)\\n return;\\n stringified(str);\\n });\\n ko.applyBindingsToNode(\\n element,\\n {\\n value: stringified,\\n valueUpdate: allBindings.get('valueUpdate'),\\n },\\n context,\\n );\\n }\\n };\\n var json_data = {\\\"formatted_vals\\\":[\\\"1\\\",\\\"2\\\",\\\"3\\\",\\\"4\\\",\\\"5\\\",\\\"6\\\",\\\"7\\\",\\\"8\\\",\\\"9\\\",\\\"10\\\"],\\\"changes\\\":WebIO.getval({\\\"name\\\":\\\"changes\\\",\\\"scope\\\":\\\"12366578372903537830\\\",\\\"id\\\":\\\"ob_02\\\",\\\"type\\\":\\\"observable\\\"}),\\\"index\\\":WebIO.getval({\\\"name\\\":\\\"index\\\",\\\"scope\\\":\\\"12366578372903537830\\\",\\\"id\\\":\\\"ob_01\\\",\\\"type\\\":\\\"observable\\\"})};\\n var self = this;\\n function AppViewModel() {\\n for (var key in json_data) {\\n var el = json_data[key];\\n this[key] = Array.isArray(el) ? ko.observableArray(el) : ko.observable(el);\\n }\\n \\n [this[\\\"formatted_val\\\"]=ko.computed( function(){\\n return this.formatted_vals()[parseInt(this.index())-(1)];\\n }\\n,this)]\\n [this[\\\"changes\\\"].subscribe((function (val){!(this.valueFromJulia[\\\"changes\\\"]) ? (WebIO.setval({\\\"name\\\":\\\"changes\\\",\\\"scope\\\":\\\"12366578372903537830\\\",\\\"id\\\":\\\"ob_02\\\",\\\"type\\\":\\\"observable\\\"},val)) : undefined; return this.valueFromJulia[\\\"changes\\\"]=false}),self),this[\\\"index\\\"].subscribe((function (val){!(this.valueFromJulia[\\\"index\\\"]) ? (WebIO.setval({\\\"name\\\":\\\"index\\\",\\\"scope\\\":\\\"12366578372903537830\\\",\\\"id\\\":\\\"ob_01\\\",\\\"type\\\":\\\"observable\\\"},val)) : undefined; return this.valueFromJulia[\\\"index\\\"]=false}),self)]\\n \\n }\\n self.model = new AppViewModel();\\n self.valueFromJulia = {};\\n for (var key in json_data) {\\n self.valueFromJulia[key] = false;\\n }\\n ko.applyBindings(self.model, self.dom);\\n}\\n);\\n (WebIO.importBlock({\\\"data\\\":[{\\\"name\\\":\\\"knockout\\\",\\\"type\\\":\\\"js\\\",\\\"url\\\":\\\"/assetserver/9a186147d51179d2ec12ccfac1799b0b805a79d5-knockout.js\\\"},{\\\"name\\\":\\\"knockout_punches\\\",\\\"type\\\":\\\"js\\\",\\\"url\\\":\\\"/assetserver/eccef011301f679ebde0900f01478cac573870ce-knockout_punches.js\\\"}],\\\"type\\\":\\\"async_block\\\"})).then((imports) => handler.apply(this, imports));\\n}\\n\")])], Dict{Symbol,Any}(:className=>\"field interact-widget\")), Observable{Any} with 0 listeners. Value:\n", "Node{DOM}(DOM(:html, :div), Any[\"The square of 5 is 25\"], Dict{Symbol,Any}(:className=>\"interact-flex-row interact-widget\"))], Dict{Symbol,Any}())" ] }, "execution_count": 1, "metadata": { "application/vnd.webio.node+json": { "kernelId": "80ee8c2b-6f83-45f4-9c3c-0c221ffb1e50" } }, "output_type": "execute_result" } ], "source": [ "using Interact\n", "\n", "@manipulate for i in 1:10\n", " j = i^2\n", " \"The square of $i is $(j)\"\n", "end" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You should see a slider appear with the label `i`. When you manipulate the slider, the caption should show the value of `i` and its square, and should update when you move the slider." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "What happened here? A `for` loop iterates over its values. Somehow the `@manipulate` command, which is a **macro**, took the object \"code for the `for` loop\" and replaced it with \"code for a slider with the same range\"; in other words, `@manipulate` operated on code to produce some different code that did something useful -- it took in a program and replaced it by a different program." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see the result of the `@manipulate` by \"expanding\" the effect of the macro using `@macroexpand`. To make it easier to read, we will suppress some line information using the `MacroTools` package:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "quote\n", " local #64#children = (Widgets.OrderedDict)(:i => (i = (Widgets.widget)(1:10, label=\"i\")))\n", " local #65#output = begin\n", " #66###364 = ((i,)->begin\n", " j = i ^ 2\n", " \"The square of $(i) is $(j)\"\n", " end)\n", " (Widgets.map)(#66###364, i)\n", " end\n", " local #67#layout = (Widgets.manipulatelayout)((Widgets.get_backend)())\n", " (Widgets.Widget){:manipulate}(#64#children, output=#65#output, layout=#67#layout)\n", "end" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "code = @macroexpand @manipulate for i in 1:10\n", " j = i^2\n", " \"The square of $i is $j\"\n", "end;\n", "\n", "using MacroTools\n", "MacroTools.striplines(code)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see that the `for` loop has been replaced by code for manipulating a widget. The information about the variable name `i`, the range `1:10` and and the code inside the `for` loop have been preserved, but they have been embedded in a certain way into a new *piece of code*." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In order to carry out a *code transformation* like this, Julia allows us to *manipulate Julia code from within Julia*: we need to be able to get inside a piece of Julia code and modify it, *before* that code reaches the Julia compiler.\n", "\n", "Finally we need to see how to wrap the result up into a macro. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Expressions " ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "application/vnd.webio.node+json": { "children": [ { "children": [ { "children": [ { "children": [ { "children": [ "i" ], "instanceArgs": { "namespace": "html", "tag": "label" }, "nodeType": "DOM", "props": { "className": "interact ", "style": { "padding": "5px 10px 0px 10px" } }, "type": "node" } ], "instanceArgs": { "namespace": "html", "tag": "div" }, "nodeType": "DOM", "props": { "className": "interact-flex-row-left" }, "type": "node" }, { "children": [ { "children": [], "instanceArgs": { "namespace": "html", "tag": "input" }, "nodeType": "DOM", "props": { "attributes": { "data-bind": "numericValue: index, valueUpdate: 'input', event: {change: function (){this.changes(this.changes()+1)}}", "orient": "horizontal", "type": "range" }, "className": "slider slider is-fullwidth", "max": 10, "min": 1, "step": 1, "style": {} }, "type": "node" } ], "instanceArgs": { "namespace": "html", "tag": "div" }, "nodeType": "DOM", "props": { "className": "interact-flex-row-center" }, "type": "node" }, { "children": [ { "children": [], "instanceArgs": { "namespace": "html", "tag": "p" }, "nodeType": "DOM", "props": { "attributes": { "data-bind": "text: formatted_val" } }, "type": "node" } ], "instanceArgs": { "namespace": "html", "tag": "div" }, "nodeType": "DOM", "props": { "className": "interact-flex-row-right" }, "type": "node" } ], "instanceArgs": { "namespace": "html", "tag": "div" }, "nodeType": "DOM", "props": { "className": "interact-flex-row interact-widget" }, "type": "node" } ], "instanceArgs": { "handlers": { "changes": [ "(function (val){return (val!=this.model[\"changes\"]()) ? (this.valueFromJulia[\"changes\"]=true, this.model[\"changes\"](val)) : undefined})" ], "index": [ "(function (val){return (val!=this.model[\"index\"]()) ? (this.valueFromJulia[\"index\"]=true, this.model[\"index\"](val)) : undefined})" ] }, "id": "12366578372903537830", "imports": { "data": [ { "name": "knockout", "type": "js", "url": "/assetserver/9a186147d51179d2ec12ccfac1799b0b805a79d5-knockout.js" }, { "name": "knockout_punches", "type": "js", "url": "/assetserver/eccef011301f679ebde0900f01478cac573870ce-knockout_punches.js" }, { "name": null, "type": "js", "url": "/assetserver/302726e4d372a82f7241f522966214f8c37abedd-all.js" }, { "name": null, "type": "css", "url": "/assetserver/2e8309e5997fb0d8cb5d92798adb0f6054224d5e-style.css" }, { "name": null, "type": "css", "url": "/assetserver/b9e7734a025cd12451b49379872363ba19d0f838-bulma_confined.min.css" } ], "type": "async_block" }, "mount_callbacks": [ "function () {\n var handler = (function (ko, koPunches) {\n ko.punches.enableAll();\n ko.bindingHandlers.numericValue = {\n init: function(element, valueAccessor, allBindings, data, context) {\n var stringified = ko.observable(ko.unwrap(valueAccessor()));\n stringified.subscribe(function(value) {\n var val = parseFloat(value);\n if (!isNaN(val)) {\n valueAccessor()(val);\n }\n });\n valueAccessor().subscribe(function(value) {\n var str = JSON.stringify(value);\n if ((str == \"0\") && ([\"-0\", \"-0.\"].indexOf(stringified()) >= 0))\n return;\n if ([\"null\", \"\"].indexOf(str) >= 0)\n return;\n stringified(str);\n });\n ko.applyBindingsToNode(\n element,\n {\n value: stringified,\n valueUpdate: allBindings.get('valueUpdate'),\n },\n context,\n );\n }\n };\n var json_data = {\"formatted_vals\":[\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"10\"],\"changes\":WebIO.getval({\"name\":\"changes\",\"scope\":\"12366578372903537830\",\"id\":\"ob_02\",\"type\":\"observable\"}),\"index\":WebIO.getval({\"name\":\"index\",\"scope\":\"12366578372903537830\",\"id\":\"ob_01\",\"type\":\"observable\"})};\n var self = this;\n function AppViewModel() {\n for (var key in json_data) {\n var el = json_data[key];\n this[key] = Array.isArray(el) ? ko.observableArray(el) : ko.observable(el);\n }\n \n [this[\"formatted_val\"]=ko.computed( function(){\n return this.formatted_vals()[parseInt(this.index())-(1)];\n }\n,this)]\n [this[\"changes\"].subscribe((function (val){!(this.valueFromJulia[\"changes\"]) ? (WebIO.setval({\"name\":\"changes\",\"scope\":\"12366578372903537830\",\"id\":\"ob_02\",\"type\":\"observable\"},val)) : undefined; return this.valueFromJulia[\"changes\"]=false}),self),this[\"index\"].subscribe((function (val){!(this.valueFromJulia[\"index\"]) ? (WebIO.setval({\"name\":\"index\",\"scope\":\"12366578372903537830\",\"id\":\"ob_01\",\"type\":\"observable\"},val)) : undefined; return this.valueFromJulia[\"index\"]=false}),self)]\n \n }\n self.model = new AppViewModel();\n self.valueFromJulia = {};\n for (var key in json_data) {\n self.valueFromJulia[key] = false;\n }\n ko.applyBindings(self.model, self.dom);\n}\n);\n (WebIO.importBlock({\"data\":[{\"name\":\"knockout\",\"type\":\"js\",\"url\":\"/assetserver/9a186147d51179d2ec12ccfac1799b0b805a79d5-knockout.js\"},{\"name\":\"knockout_punches\",\"type\":\"js\",\"url\":\"/assetserver/eccef011301f679ebde0900f01478cac573870ce-knockout_punches.js\"}],\"type\":\"async_block\"})).then((imports) => handler.apply(this, imports));\n}\n" ], "observables": { "changes": { "id": "ob_02", "sync": false, "value": 2 }, "index": { "id": "ob_01", "sync": true, "value": 5 } }, "systemjs_options": null }, "nodeType": "Scope", "props": {}, "type": "node" } ], "instanceArgs": { "namespace": "html", "tag": "div" }, "nodeType": "DOM", "props": { "className": "field interact-widget" }, "type": "node" }, "text/html": [ "\n", " \n", "\n" ], "text/plain": [ "Node{WebIO.DOM}(WebIO.DOM(:html, :div), Any[Scope(Node{DOM}(DOM(:html, :div), Any[Node{DOM}(DOM(:html, :div), Any[Node{DOM}(DOM(:html, :label), Any[\"i\"], Dict{Symbol,Any}(:className=>\"interact \",:style=>Dict{Any,Any}(:padding=>\"5px 10px 0px 10px\")))], Dict{Symbol,Any}(:className=>\"interact-flex-row-left\")), Node{DOM}(DOM(:html, :div), Any[Node{DOM}(DOM(:html, :input), Any[], Dict{Symbol,Any}(:max=>10,:min=>1,:attributes=>Dict{Any,Any}(:type=>\"range\",Symbol(\"data-bind\")=>\"numericValue: index, valueUpdate: 'input', event: {change: function (){this.changes(this.changes()+1)}}\",\"orient\"=>\"horizontal\"),:step=>1,:className=>\"slider slider is-fullwidth\",:style=>Dict{Any,Any}()))], Dict{Symbol,Any}(:className=>\"interact-flex-row-center\")), Node{DOM}(DOM(:html, :div), Any[Node{DOM}(DOM(:html, :p), Any[], Dict{Symbol,Any}(:attributes=>Dict(\"data-bind\"=>\"text: formatted_val\")))], Dict{Symbol,Any}(:className=>\"interact-flex-row-right\"))], Dict{Symbol,Any}(:className=>\"interact-flex-row interact-widget\")), Dict{String,Tuple{Observables.AbstractObservable,Union{Nothing, Bool}}}(\"changes\"=>(Observable{Int64} with 1 listeners. Value:\n", "2, nothing),\"index\"=>(Observable{Int64} with 2 listeners. Value:\n", "5, nothing)), Set(String[]), nothing, Asset[Asset(\"js\", \"knockout\", \"/Users/dpsanders/.julia/packages/Knockout/1sDlc/src/../assets/knockout.js\"), Asset(\"js\", \"knockout_punches\", \"/Users/dpsanders/.julia/packages/Knockout/1sDlc/src/../assets/knockout_punches.js\"), Asset(\"js\", nothing, \"/Users/dpsanders/.julia/packages/InteractBase/l8cEC/src/../assets/all.js\"), Asset(\"css\", nothing, \"/Users/dpsanders/.julia/packages/InteractBase/l8cEC/src/../assets/style.css\"), Asset(\"css\", nothing, \"/Users/dpsanders/.julia/packages/Interact/0klKX/src/../assets/bulma_confined.min.css\")], Dict{Any,Any}(\"changes\"=>Any[JSString(\"(function (val){return (val!=this.model[\\\"changes\\\"]()) ? (this.valueFromJulia[\\\"changes\\\"]=true, this.model[\\\"changes\\\"](val)) : undefined})\")],\"index\"=>Any[JSString(\"(function (val){return (val!=this.model[\\\"index\\\"]()) ? (this.valueFromJulia[\\\"index\\\"]=true, this.model[\\\"index\\\"](val)) : undefined})\")]), ConnectionPool(Channel{Any}(sz_max:9223372036854775807,sz_curr:0), Set(AbstractConnection[]), Channel{AbstractConnection}(sz_max:32,sz_curr:1)), WebIO.JSString[JSString(\"function () {\\n var handler = (function (ko, koPunches) {\\n ko.punches.enableAll();\\n ko.bindingHandlers.numericValue = {\\n init: function(element, valueAccessor, allBindings, data, context) {\\n var stringified = ko.observable(ko.unwrap(valueAccessor()));\\n stringified.subscribe(function(value) {\\n var val = parseFloat(value);\\n if (!isNaN(val)) {\\n valueAccessor()(val);\\n }\\n });\\n valueAccessor().subscribe(function(value) {\\n var str = JSON.stringify(value);\\n if ((str == \\\"0\\\") && ([\\\"-0\\\", \\\"-0.\\\"].indexOf(stringified()) >= 0))\\n return;\\n if ([\\\"null\\\", \\\"\\\"].indexOf(str) >= 0)\\n return;\\n stringified(str);\\n });\\n ko.applyBindingsToNode(\\n element,\\n {\\n value: stringified,\\n valueUpdate: allBindings.get('valueUpdate'),\\n },\\n context,\\n );\\n }\\n };\\n var json_data = {\\\"formatted_vals\\\":[\\\"1\\\",\\\"2\\\",\\\"3\\\",\\\"4\\\",\\\"5\\\",\\\"6\\\",\\\"7\\\",\\\"8\\\",\\\"9\\\",\\\"10\\\"],\\\"changes\\\":WebIO.getval({\\\"name\\\":\\\"changes\\\",\\\"scope\\\":\\\"12366578372903537830\\\",\\\"id\\\":\\\"ob_02\\\",\\\"type\\\":\\\"observable\\\"}),\\\"index\\\":WebIO.getval({\\\"name\\\":\\\"index\\\",\\\"scope\\\":\\\"12366578372903537830\\\",\\\"id\\\":\\\"ob_01\\\",\\\"type\\\":\\\"observable\\\"})};\\n var self = this;\\n function AppViewModel() {\\n for (var key in json_data) {\\n var el = json_data[key];\\n this[key] = Array.isArray(el) ? ko.observableArray(el) : ko.observable(el);\\n }\\n \\n [this[\\\"formatted_val\\\"]=ko.computed( function(){\\n return this.formatted_vals()[parseInt(this.index())-(1)];\\n }\\n,this)]\\n [this[\\\"changes\\\"].subscribe((function (val){!(this.valueFromJulia[\\\"changes\\\"]) ? (WebIO.setval({\\\"name\\\":\\\"changes\\\",\\\"scope\\\":\\\"12366578372903537830\\\",\\\"id\\\":\\\"ob_02\\\",\\\"type\\\":\\\"observable\\\"},val)) : undefined; return this.valueFromJulia[\\\"changes\\\"]=false}),self),this[\\\"index\\\"].subscribe((function (val){!(this.valueFromJulia[\\\"index\\\"]) ? (WebIO.setval({\\\"name\\\":\\\"index\\\",\\\"scope\\\":\\\"12366578372903537830\\\",\\\"id\\\":\\\"ob_01\\\",\\\"type\\\":\\\"observable\\\"},val)) : undefined; return this.valueFromJulia[\\\"index\\\"]=false}),self)]\\n \\n }\\n self.model = new AppViewModel();\\n self.valueFromJulia = {};\\n for (var key in json_data) {\\n self.valueFromJulia[key] = false;\\n }\\n ko.applyBindings(self.model, self.dom);\\n}\\n);\\n (WebIO.importBlock({\\\"data\\\":[{\\\"name\\\":\\\"knockout\\\",\\\"type\\\":\\\"js\\\",\\\"url\\\":\\\"/assetserver/9a186147d51179d2ec12ccfac1799b0b805a79d5-knockout.js\\\"},{\\\"name\\\":\\\"knockout_punches\\\",\\\"type\\\":\\\"js\\\",\\\"url\\\":\\\"/assetserver/eccef011301f679ebde0900f01478cac573870ce-knockout_punches.js\\\"}],\\\"type\\\":\\\"async_block\\\"})).then((imports) => handler.apply(this, imports));\\n}\\n\")])], Dict{Symbol,Any}(:className=>\"field interact-widget\"))" ] }, "execution_count": 6, "metadata": { "application/vnd.webio.node+json": { "kernelId": "80ee8c2b-6f83-45f4-9c3c-0c221ffb1e50" } }, "output_type": "execute_result" } ], "source": [ "i" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "ename": "MethodError", "evalue": "MethodError: no method matching ^(::Widget{:slider,Int64}, ::Int64)\nClosest candidates are:\n ^(!Matched::Float16, ::Integer) at math.jl:795\n ^(!Matched::Missing, ::Integer) at missing.jl:124\n ^(!Matched::Missing, ::Number) at missing.jl:97\n ...", "output_type": "error", "traceback": [ "MethodError: no method matching ^(::Widget{:slider,Int64}, ::Int64)\nClosest candidates are:\n ^(!Matched::Float16, ::Integer) at math.jl:795\n ^(!Matched::Missing, ::Integer) at missing.jl:124\n ^(!Matched::Missing, ::Number) at missing.jl:97\n ...", "", "Stacktrace:", " [1] macro expansion at ./none:0 [inlined]", " [2] literal_pow(::typeof(^), ::Widget{:slider,Int64}, ::Val{2}) at ./none:0", " [3] top-level scope at In[5]:1" ] } ], "source": [ "j = i^2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's start with just the part `j = i^2`. If we type this code into a *fresh* Julia session, we get the following error:\n", "```\n", "julia> j = i^2\n", "ERROR: UndefVarError: i not defined\n", "Stacktrace:\n", " [1] top-level scope at none:0\n", "```\n", "since Julia is trying to *evaluate* the code using the values for the variables `i` and `j`, which are not defined.\n", "\n", "[If instead we type this after running the above `@manipulate` command, `i` is interpreted as a slider and we get a different error.]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For metaprogramming purposes, we do not wish to *evaluate* the code; instead, we just want to treat the code as unevaluated symbolic expressions, which will gain meaning only later. Julia allows us to construct unevaluated pieces of code as follows:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "quote\n", " #= In[7]:2 =#\n", " j = i ^ 2\n", "end" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "quote \n", " j = i^2\n", "end" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "or with the following shorthand syntax:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":(j = i ^ 2)" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ ":(j = i^2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Exercise 2\n", "\n", "1. Define a variable `code` to be `:(j = i^2)`. \n", "\n", "\n", "2. What type is the object `code`? Note that `code` is just a normal Julia variable, of a particular special type.\n", "\n", "\n", "3. Use the `dump` function to see what there is inside `code`. \n", "Remembering that `code` is just a particular kind of Julia object, use the Julia to play around interactively, seeing how you can extract pieces of the `code` object.\n", "\n", "\n", "4. How is the operation `i^2` represented? What kind of object is that subpiece?\n", "\n", "\n", "5. Copy `code` into a variable `code2`. *Modify* this to replace the power `2` with a power `3`. Make sure that the original `code` variable is *not* also modified.\n", "\n", "\n", "6. Copy `code2` to a variable `code3`. Replace `i` with `i + 1` in `code3`.\n", "\n", "\n", "7. Define a variable `i` with the value `4`. *Evaluate* the different `code` expressions using the `eval` function and check the *value* of the variable `j`.\n" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":(j = i ^ 2)" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "code = Meta.parse(\"j = i^2\")" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Expr" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "typeof(code)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Expr\n", " head: Symbol =\n", " args: Array{Any}((2,))\n", " 1: Symbol j\n", " 2: Expr\n", " head: Symbol call\n", " args: Array{Any}((3,))\n", " 1: Symbol ^\n", " 2: Symbol i\n", " 3: Int64 2\n" ] } ], "source": [ "dump(code)" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":(=)" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "code.head" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Symbol" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "typeof(ans)" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":+" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ ":+" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Symbol" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "typeof(ans)" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "+ (generic function with 171 methods)" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "+" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "2-element Array{Any,1}:\n", " :j \n", " :(i ^ 2)" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "code.args" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Array{Any,1}" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "typeof(code.args)" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":j" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "code.args[1]" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Symbol" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "typeof(code.args[1])" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":(i ^ 2)" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "code.args[2]" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Expr" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "typeof(code.args[2])" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":call" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "code.args[2].head" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "3-element Array{Any,1}:\n", " :^\n", " :i\n", " 2 " ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "code.args[2].args" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "2" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "code.args[2].args[3]" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "3" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "code.args[2].args[3] = 3" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":(j = i ^ 3)" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "code" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":(i + 1)" ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "code.args[2].args[2] = :(i + 1)" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":(j = (i + 1) ^ 3)" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "code" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Expr\n", " head: Symbol =\n", " args: Array{Any}((2,))\n", " 1: Symbol j\n", " 2: Expr\n", " head: Symbol call\n", " args: Array{Any}((3,))\n", " 1: Symbol ^\n", " 2: Expr\n", " head: Symbol call\n", " args: Array{Any}((3,))\n", " 1: Symbol +\n", " 2: Symbol i\n", " 3: Int64 1\n", " 3: Int64 3\n" ] } ], "source": [ "dump(ans)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We have just taken a (very simple) program, represented by `code`, and produced two new programs, `code2` and `code3`, which we ran using `eval`. This is the basis of all metaprogramming: taking in a piece of code, and modifying it to produce a new piece of code." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Walking a syntax tree " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the previous exercise, we modified a single `i` to `i + 1`. But in general we might have a more complicated expression like `i^2 + (i * (i - 3))` and we may wish to modify *all* of the `i`s in the expression to `(i+1)`s or to `k`s to produce the new expression. The problem is that they may be buried arbitrarily deeply. We thus need to find a way of walking through the whole expression to examine each subpiece of it. " ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":(i ^ 2 + i * (i - 3))" ] }, "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ "code = :( i^2 + (i * (i - 3)) )" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Expr\n", " head: Symbol call\n", " args: Array{Any}((3,))\n", " 1: Symbol +\n", " 2: Expr\n", " head: Symbol call\n", " args: Array{Any}((3,))\n", " 1: Symbol ^\n", " 2: Symbol i\n", " 3: Int64 2\n", " 3: Expr\n", " head: Symbol call\n", " args: Array{Any}((3,))\n", " 1: Symbol *\n", " 2: Symbol i\n", " 3: Expr\n", " head: Symbol call\n", " args: Array{Any}((3,))\n", " 1: Symbol -\n", " 2: Symbol i\n", " 3: Int64 3\n" ] } ], "source": [ "dump(code)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Exercise 3 \n", "\n", "1. Write a function `walk!` that takes an expression object and replaces **all** of the `:x`s by `:z`s. \n", "\n", " Hint: This function should be *recursive*: at some point it will need to call itself if it finds that a piece of the expression is itself an `Expr`.\n", " \n", " \n", "2. Make this function into a more general pattern matcher that looks for a given sub-expression and replaces it by another." ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "false" ] }, "execution_count": 40, "metadata": {}, "output_type": "execute_result" } ], "source": [ "3 isa Expr" ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "false" ] }, "execution_count": 41, "metadata": {}, "output_type": "execute_result" } ], "source": [ "3 isa Symbol" ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "true" ] }, "execution_count": 42, "metadata": {}, "output_type": "execute_result" } ], "source": [ "3 isa Number" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "false" ] }, "execution_count": 43, "metadata": {}, "output_type": "execute_result" } ], "source": [ ":x isa Expr" ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "true" ] }, "execution_count": 44, "metadata": {}, "output_type": "execute_result" } ], "source": [ ":x isa Symbol" ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "true" ] }, "execution_count": 45, "metadata": {}, "output_type": "execute_result" } ], "source": [ ":(x + 1) isa Expr" ] }, { "cell_type": "code", "execution_count": 50, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "walk! (generic function with 1 method)" ] }, "execution_count": 50, "metadata": {}, "output_type": "execute_result" } ], "source": [ "function walk!(ex::Expr)\n", " for arg in ex.args\n", " @show arg\n", " if arg == :x\n", " arg = :z\n", " end\n", " \n", " if arg isa Expr\n", " walk!(arg)\n", " end\n", " end\n", "end" ] }, { "cell_type": "code", "execution_count": 51, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":(x * x + x)" ] }, "execution_count": 51, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ex = :(x*x + x)" ] }, { "cell_type": "code", "execution_count": 52, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "arg = :+\n", "arg = :(x * x)\n", "arg = :*\n", "arg = :x\n", "arg = :x\n", "arg = :x\n" ] } ], "source": [ "walk!(ex)" ] }, { "cell_type": "code", "execution_count": 49, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":(x * x + x)" ] }, "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ex" ] }, { "cell_type": "code", "execution_count": 82, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "walk! (generic function with 1 method)" ] }, "execution_count": 82, "metadata": {}, "output_type": "execute_result" } ], "source": [ "function walk!(ex::Expr)\n", " \n", " args = ex.args\n", " \n", " for i in 1:length(args)\n", "\n", " if args[i] == :x\n", " args[i] = :z\n", " end\n", " \n", " if args[i] isa Expr\n", " walk!(args[i])\n", " end\n", " end\n", " \n", " return ex\n", "end" ] }, { "cell_type": "code", "execution_count": 62, "metadata": {}, "outputs": [], "source": [ "walk!(ex)" ] }, { "cell_type": "code", "execution_count": 64, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":(z * z + z)" ] }, "execution_count": 64, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ex" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Exercise 4\n", "Julia by default uses standard 64-bit (or 32-bit) integers, which leads to surprising overflow behaviour, e.g." ] }, { "cell_type": "code", "execution_count": 76, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "-9223372036854775808" ] }, "execution_count": 76, "metadata": {}, "output_type": "execute_result" } ], "source": [ "2^32 * 2^31" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "No warning is given that there was an overflow in this calculation. \n", "\n", "However, in `Base` there are *checked* operations, such as `checked_mul`, which do throw an exception on overflow:" ] }, { "cell_type": "code", "execution_count": 65, "metadata": {}, "outputs": [ { "ename": "OverflowError", "evalue": "OverflowError: 1152921504606846976 *y overflowed for type Int64", "output_type": "error", "traceback": [ "OverflowError: 1152921504606846976 *y overflowed for type Int64", "", "Stacktrace:", " [1] throw_overflowerr_binaryop(::Symbol, ::Int64, ::Int64) at ./checked.jl:154", " [2] checked_mul(::Int64, ::Int64) at ./checked.jl:288", " [3] top-level scope at In[65]:1" ] } ], "source": [ "Base.checked_mul(2^60, 2^60)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "1. Write a function `make_checked` that replaces standard functions (`-`, `+`, `*`, `/`) in an expression by their corresponding checked counterparts." ] }, { "cell_type": "code", "execution_count": 66, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":(i ^ 2 + i * (i - 3))" ] }, "execution_count": 66, "metadata": {}, "output_type": "execute_result" } ], "source": [ "code" ] }, { "cell_type": "code", "execution_count": 67, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":(x + x * x)" ] }, "execution_count": 67, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ex = :(x + x * x)" ] }, { "cell_type": "code", "execution_count": 68, "metadata": {}, "outputs": [ { "ename": "UndefVarError", "evalue": "UndefVarError: x not defined", "output_type": "error", "traceback": [ "UndefVarError: x not defined", "", "Stacktrace:", " [1] top-level scope at none:0", " [2] eval at ./boot.jl:328 [inlined]", " [3] eval(::Expr) at ./client.jl:404", " [4] top-level scope at In[68]:1" ] } ], "source": [ "eval(ex)" ] }, { "cell_type": "code", "execution_count": 69, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "3" ] }, "execution_count": 69, "metadata": {}, "output_type": "execute_result" } ], "source": [ "x = 3" ] }, { "cell_type": "code", "execution_count": 70, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "12" ] }, "execution_count": 70, "metadata": {}, "output_type": "execute_result" } ], "source": [ "eval(ex)" ] }, { "cell_type": "code", "execution_count": 71, "metadata": {}, "outputs": [], "source": [ "walk!(ex)" ] }, { "cell_type": "code", "execution_count": 72, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":(z + z * z)" ] }, "execution_count": 72, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ex" ] }, { "cell_type": "code", "execution_count": 73, "metadata": {}, "outputs": [ { "ename": "UndefVarError", "evalue": "UndefVarError: z not defined", "output_type": "error", "traceback": [ "UndefVarError: z not defined", "", "Stacktrace:", " [1] top-level scope at In[73]:1" ] } ], "source": [ "eval(z)" ] }, { "cell_type": "code", "execution_count": 74, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "5" ] }, "execution_count": 74, "metadata": {}, "output_type": "execute_result" } ], "source": [ "z = 5" ] }, { "cell_type": "code", "execution_count": 75, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "5" ] }, "execution_count": 75, "metadata": {}, "output_type": "execute_result" } ], "source": [ "eval(z)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Generating repetitive code" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "One common application of basic metaprogramming in Julia is generating repetitive code. For example, there are situations in which it's useful to wrap on object of one type into a user-defined type in order to modify its behaviour in some way. \n", "\n", "e.g. Let's define a wrapper type `MyFloat` of `Float64`:" ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [], "source": [ "struct MyFloat\n", " x::Float64\n", "end" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can generate objects of this type:" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "MyFloat(4.0)" ] }, "execution_count": 43, "metadata": {}, "output_type": "execute_result" } ], "source": [ "a = MyFloat(3)\n", "b = MyFloat(4)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "But arithmetic operations are not defined:" ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [ { "ename": "MethodError", "evalue": "MethodError: no method matching +(::MyFloat, ::MyFloat)\nClosest candidates are:\n +(::Any, ::Any, !Matched::Any, !Matched::Any...) at operators.jl:502", "output_type": "error", "traceback": [ "MethodError: no method matching +(::MyFloat, ::MyFloat)\nClosest candidates are:\n +(::Any, ::Any, !Matched::Any, !Matched::Any...) at operators.jl:502", "", "Stacktrace:", " [1] top-level scope at In[44]:1" ] } ], "source": [ "a + b" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can define them in the natural way:" ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "- (generic function with 188 methods)" ] }, "execution_count": 45, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import Base: +, -, *, /\n", "\n", "+(a::MyFloat, b::MyFloat) = a.x + b.x\n", "-(a::MyFloat, b::MyFloat) = a.x - b.x" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "But this will quickly get dull, and we could easily make a mistake. \n", "As usual, whenever we are repeating something more than twice, we should try to automate it.\n", "\n", "We have some code of the form\n", "\n", " op(a::MyFloat, b::MyFloat) = op(a.x, b.x)\n", " \n", "where `op` denotes the operator. Julia allows us to do this almost literally; we just need to substitute in the *value* of the *variable* `op`! " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Exercise 5\n", "\n", "1. Let `op` be the symbol `:+`, which is an unevaluated version of the `+` operator.\n", "\n", "\n", "2. Let `code` be the expression corresponding to `op(a::MyFloat, b::MyFloat) = op(a.x, b.x)`.\n", "\n", "\n", "3. Substitute the *value* of `op` by replacing `op` by `$(op)`. Check that `code` contains the correct result.\n", "\n", "\n", "4. Evaluate the code in order to generate the new method. Check that the method works for objects of type `MyFloat`.\n", "\n", "\n", "5. We can replace the two steps \"define `code`\" and \"evaluate `code`\" by one step, `@eval` of the expression that defines `code`.\n", "\n", "\n", "6. Write a loop over the operations `+`, `-`, `*` and `/` to define them all for our wrapper type." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally we need to evaluate the code. The combination of `eval` and `:(...)` that we used above can be abbreviated to `@eval`:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Macros" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally let's return to macros. Recall that macros begin with `@` and behave like \"super-functions\", which take in a piece of code and replace it with another piece of code.\n", "In fact, the effect of a macro call will be to insert the new piece of code in place of the old code, which is consequently compiled by the Julia compiler. \n", "\n", "Note that the user *does not need to explicitly pass an `Expr`ession object*; Julia turns the code that follows the macro call into an expression." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To see this, let's define the simplest macro:" ] }, { "cell_type": "code", "execution_count": 89, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "@simple (macro with 1 method)" ] }, "execution_count": 89, "metadata": {}, "output_type": "execute_result" } ], "source": [ "macro simple(expr)\n", " @show expr, typeof(expr)\n", " nothing # return nothing for the moment\n", "end" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "and run it with the following simple code:" ] }, { "cell_type": "code", "execution_count": 91, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(expr, typeof(expr)) = (:(yy = xx ^ 2), Expr)\n" ] } ], "source": [ "result = @simple yy = xx^2" ] }, { "cell_type": "code", "execution_count": 92, "metadata": {}, "outputs": [], "source": [ "result" ] }, { "cell_type": "code", "execution_count": 86, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "@walk! (macro with 1 method)" ] }, "execution_count": 86, "metadata": {}, "output_type": "execute_result" } ], "source": [ "macro walk!(expr)\n", " @show expr\n", " result = walk!(expr)\n", " @show result\n", " return result\n", "end" ] }, { "cell_type": "code", "execution_count": 87, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "expr = :(x + x)\n", "result = :(z + z)\n" ] }, { "data": { "text/plain": [ "10" ] }, "execution_count": 87, "metadata": {}, "output_type": "execute_result" } ], "source": [ "@walk! x + x" ] }, { "cell_type": "code", "execution_count": 88, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "expr = :(x + x)\n", "result = :(z + z)\n" ] }, { "data": { "text/plain": [ ":(Main.z + Main.z)" ] }, "execution_count": 88, "metadata": {}, "output_type": "execute_result" } ], "source": [ "@macroexpand @walk! x + x" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see that the Julia code that follows the macro call is passed to the macro, *already having been parsed into an `Expr` object*." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Exercise 6" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "1. Define a macro `@simple2` that returns the expression that was passed to it.\n", "\n", "\n", "2. What happens when you call `@simple2 yy = xx^2`?\n", "\n", "\n", "3. Define a variable `xx` with the value `3`. Does the macro work now?\n", "\n", "\n", "4. Does the variable `xx` now exist?\n", "\n", "\n", "5. To see what's happening, use `@macroexpand`." ] }, { "cell_type": "code", "execution_count": 93, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "@simple2 (macro with 1 method)" ] }, "execution_count": 93, "metadata": {}, "output_type": "execute_result" } ], "source": [ "macro simple2(expr)\n", " return expr\n", "end" ] }, { "cell_type": "code", "execution_count": 94, "metadata": {}, "outputs": [ { "ename": "UndefVarError", "evalue": "UndefVarError: xx not defined", "output_type": "error", "traceback": [ "UndefVarError: xx not defined", "", "Stacktrace:", " [1] top-level scope at In[94]:1" ] } ], "source": [ "@simple2 yy = xx^2" ] }, { "cell_type": "code", "execution_count": 95, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "10" ] }, "execution_count": 95, "metadata": {}, "output_type": "execute_result" } ], "source": [ "xx = 10" ] }, { "cell_type": "code", "execution_count": 96, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "100" ] }, "execution_count": 96, "metadata": {}, "output_type": "execute_result" } ], "source": [ "@simple2 yy = xx^2" ] }, { "cell_type": "code", "execution_count": 97, "metadata": {}, "outputs": [ { "ename": "UndefVarError", "evalue": "UndefVarError: yy not defined", "output_type": "error", "traceback": [ "UndefVarError: yy not defined", "", "Stacktrace:", " [1] top-level scope at In[97]:1" ] } ], "source": [ "yy" ] }, { "cell_type": "code", "execution_count": 98, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":(#79#yy = Main.xx ^ 2)" ] }, "execution_count": 98, "metadata": {}, "output_type": "execute_result" } ], "source": [ "@macroexpand @simple2 yy = xx^2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You should find that the variable `yy` does *not* now exist, even though it seems like it should, since the code `yy = xx^2` was evaluated. However, macros by default do not \"touch\" variables in the context from where they are called, since this may have unintended consequences. We refer to macro **hygiene** (they do not \"infect\" code where they are not welcome).\n", "\n", "Nonetheless, often we may *wish* them to modify variables in the context from which they are called, in which case we can \"escape\" from this hygiene using `esc`:" ] }, { "cell_type": "code", "execution_count": 109, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":+" ] }, "execution_count": 109, "metadata": {}, "output_type": "execute_result" } ], "source": [ "op = :+" ] }, { "cell_type": "code", "execution_count": 110, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":(x + y)" ] }, "execution_count": 110, "metadata": {}, "output_type": "execute_result" } ], "source": [ "code = :($op(x, y))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": 103, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "@simple3 (macro with 1 method)" ] }, "execution_count": 103, "metadata": {}, "output_type": "execute_result" } ], "source": [ "macro simple3(expr)\n", " return :($(esc(expr)))\n", "end" ] }, { "cell_type": "code", "execution_count": 104, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "100" ] }, "execution_count": 104, "metadata": {}, "output_type": "execute_result" } ], "source": [ "@simple3 yy = xx^2" ] }, { "cell_type": "code", "execution_count": 105, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "100" ] }, "execution_count": 105, "metadata": {}, "output_type": "execute_result" } ], "source": [ "yy" ] }, { "cell_type": "code", "execution_count": 107, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":(yy = xx ^ 2)" ] }, "execution_count": 107, "metadata": {}, "output_type": "execute_result" } ], "source": [ "@macroexpand @simple3 yy = xx^2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that once again the macro must return an *expression*. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Exercise 7\n", "\n", "1. Check that `@simple3` does create a variable `yy`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When writing macros, it is common to treat the macro as simply a wrapper around a function that does the hard work of transforming one `Expr` into another`:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Exercise 8\n", "\n", "1. Write a macro `@walk!` that uses the function `walk!` defined above to replace terms in an expression. Apply it to `yy = xx^2`, replacing `xx` by `xx + 1`.\n", "\n", "\n", "2. Write a macro `@checked` that replaces all arithmetic operations with checked operations." ] } ], "metadata": { "@webio": { "lastCommId": "3d113b67d1cc480387e771408a0fa18a", "lastKernelId": "80ee8c2b-6f83-45f4-9c3c-0c221ffb1e50" }, "kernelspec": { "display_name": "Julia 1.1.0", "language": "julia", "name": "julia-1.1" }, "language_info": { "file_extension": ".jl", "mimetype": "application/julia", "name": "julia", "version": "1.1.0" }, "toc": { "colors": { "hover_highlight": "#DAA520", "running_highlight": "#FF0000", "selected_highlight": "#FFD700" }, "moveMenuLeft": true, "nav_menu": { "height": "245px", "width": "251px" }, "navigate_menu": true, "number_sections": true, "sideBar": true, "threshold": "2", "toc_cell": false, "toc_section_display": "block", "toc_window_display": false } }, "nbformat": 4, "nbformat_minor": 1 }