{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Multiple dispatch" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this notebook we'll explore **multiple dispatch**, which is a key feature of Julia.\n", "\n", "Multiple dispatch makes software *generic* and *fast*!\n", "\n", "#### Starting with the familiar\n", "\n", "To understand multiple dispatch in Julia, let's start with what we've already seen.\n", "\n", "We can declare functions in Julia without giving Julia any information about the types of the input arguments that function will receive:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "f (generic function with 1 method)" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "f(x) = x^2" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "ename": "ArgumentError", "evalue": "ArgumentError: Package InteractiveShell not found in current path:\n- Run `import Pkg; Pkg.add(\"InteractiveShell\")` to install the InteractiveShell package.\n", "output_type": "error", "traceback": [ "ArgumentError: Package InteractiveShell not found in current path:\n- Run `import Pkg; Pkg.add(\"InteractiveShell\")` to install the InteractiveShell package.\n", "", "Stacktrace:", " [1] require(::Module, ::Symbol) at .\\loading.jl:892", " [2] top-level scope at In[38]:1" ] } ], "source": [ "import InteractiveShell\n", "InteractiveShell.ast_node_interactivity = \"all\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "and then Julia will determine on its own which input argument types make sense and which do not:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "100" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "f(10)" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "scrolled": true }, "outputs": [ { "ename": "MethodError", "evalue": "MethodError: no method matching ^(::Array{Int64,1}, ::Int64)\nClosest candidates are:\n ^(!Matched::Float16, ::Integer) at math.jl:885\n ^(!Matched::Regex, ::Integer) at regex.jl:712\n ^(!Matched::Missing, ::Integer) at missing.jl:155\n ...", "output_type": "error", "traceback": [ "MethodError: no method matching ^(::Array{Int64,1}, ::Int64)\nClosest candidates are:\n ^(!Matched::Float16, ::Integer) at math.jl:885\n ^(!Matched::Regex, ::Integer) at regex.jl:712\n ^(!Matched::Missing, ::Integer) at missing.jl:155\n ...", "", "Stacktrace:", " [1] macro expansion at .\\none:0 [inlined]", " [2] literal_pow at .\\none:0 [inlined]", " [3] f(::Array{Int64,1}) at .\\In[1]:1", " [4] top-level scope at In[3]:1" ] } ], "source": [ "f([1, 2, 3])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Specifying the types of our input arguments\n", "\n", "However, we also have the *option* to tell Julia explicitly what types our input arguments are allowed to have.\n", "\n", "For example, let's write a function `foo` that only takes strings as inputs." ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "foo (generic function with 4 methods)" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "foo(x::String, y::String) = println(\"My inputs x and y are both strings!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see here that in order to restrict the type of `x` and `y` to `String`s, we just follow the input argument name by a double colon and the keyword `String`.\n", "\n", "Now we'll see that `foo` works on `String`s and doesn't work on other input argument types." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "My inputs x and y are both strings!\n" ] } ], "source": [ "foo(\"hello\", \"hi!\")" ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "My inputs x and y are both integers!\n", "foo(3, 4) = nothing\n" ] }, { "data": { "text/plain": [ "foo (generic function with 4 methods)" ] }, "execution_count": 45, "metadata": {}, "output_type": "execute_result" } ], "source": [ "@show foo(3, 4)\n", "foo(x, y) = x^y" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To get `foo` to work on integer (`Int`) inputs, let's tack `::Int` onto our input arguments when we declare `foo`." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "foo (generic function with 3 methods)" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "foo(x::Int, y::Int) = println(\"My inputs x and y are both integers!\")" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "My inputs x and y are both integers!\n" ] } ], "source": [ "foo(3, 4)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now `foo` works on integers! But look, `foo` also still works when `x` and `y` are strings!" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "My inputs x and y are both strings!\n" ] } ], "source": [ "foo(\"hello\", \"hi!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is starting to get to the heart of multiple dispatch. When we declared\n", "\n", "```julia\n", "foo(x::Int, y::Int) = println(\"My inputs x and y are both integers!\")\n", "```\n", "we didn't overwrite or replace\n", "```julia\n", "foo(y::String, y::String)\n", "```\n", "Instead, we just added an additional ***method*** to the ***generic function*** called `foo`.\n", "\n", "A ***generic function*** is the abstract concept associated with a particular operation.\n", "\n", "For example, the generic function `+` represents the concept of addition.\n", "\n", "A ***method*** is a specific implementation of a generic function for *particular argument types*.\n", "\n", "For example, `+` has methods that accept floating point numbers, integers, matrices, etc.\n", "\n", "We can use the `methods` to see how many methods there are for `foo`." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/html": [ "# 3 methods for generic function foo:" ], "text/plain": [ "# 3 methods for generic function \"foo\":\n", "[1] foo(x::Int64, y::Int64) in Main at In[12]:1\n", "[2] foo(x::String, y::String) in Main at In[4]:1\n", "[3] foo(x, y) in Main at In[11]:2" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "methods(foo)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Aside: how many methods do you think there are for addition?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "methods(+)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So, we now can call `foo` on integers or strings. When you call `foo` on a particular set of arguments, Julia will infer the types of the inputs and dispatch the appropriate method. *This* is multiple dispatch.\n", "\n", "Multiple dispatch makes our code generic and fast. Our code can be generic and flexible because we can write code in terms of abstract operations such as addition and multiplication, rather than in terms of specific implementations. At the same time, our code runs quickly because Julia is able to call efficient methods for the relevant types.\n", "\n", "To see which method is being dispatched when we call a generic function, we can use the @which macro:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/html": [ "foo(x::Int64, y::Int64) in Main at In[12]:1" ], "text/plain": [ "foo(x::Int64, y::Int64) in Main at In[12]:1" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "@which foo(3, 4)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's see what happens when we use `@which` with the addition operator!" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/html": [ "+(x::Float64, y::Float64) in Base at float.jl:401" ], "text/plain": [ "+(x::Float64, y::Float64) in Base at float.jl:401" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "@which 3.0 + 3.0" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And we can continue to add other methods to our generic function `foo`. Let's add one that takes the ***abstract type*** `Number`, which includes subtypes such as `Int`, `Float64`, and other objects you would think of as numbers:" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "foo (generic function with 4 methods)" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "foo(x::Number, y::Number) = println(\"My inputs x and y are both numbers!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This method for `foo` will work on, for example, floating point numbers:" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "My inputs x and y are both numbers!\n" ] } ], "source": [ "foo(3, 4.0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can also add a fallback, duck-typed method for `foo` that takes inputs of any type:" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "foo (generic function with 4 methods)" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "foo(x, y) = println(\"I accept inputs of any type!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Given the methods we've already written for `foo` so far, this method will be called whenever we pass non-numbers to `foo`:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "v = rand(3) = [0.06585662005971038, 0.05853846373676874, 0.6688305919777056]\n" ] }, { "ename": "UndefVarError", "evalue": "UndefVarError: foo not defined", "output_type": "error", "traceback": [ "UndefVarError: foo not defined", "", "Stacktrace:", " [1] top-level scope at In[4]:2" ] } ], "source": [ "@show v = rand(3)\n", "foo(v, v)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Exercises\n", "\n", "#### 9.1\n", "\n", "Extend the function `foo`, adding a method that takes only one input argument, which is of type `Bool`, and returns \"foo with one boolean!\"" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "foo (generic function with 1 method)" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "foo(x::Bool) = \"foo with one boolean!\"" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "foo(true) = \"foo with one boolean!\"\n", "false = false\n" ] }, { "data": { "text/plain": [ "\"foo with one boolean!\"" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "@show foo(true)\n", "foo(@show false)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 9.2\n", "\n", "Check that the method being dispatched when you execute \n", "```julia\n", "foo(true)\n", "```\n", "is the one you wrote." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "\"foo with one boolean!\"" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "foo(true)" ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "deletable": false, "editable": false, "hide_input": true, "nbgrader": { "checksum": "af0db7a08de56491e57f4c882296a00f", "grade": true, "grade_id": "cell-14072e60ae07c1a2", "locked": true, "points": 1, "schema_version": 1, "solution": false } }, "outputs": [], "source": [ "@assert foo(true) == \"foo with one boolean!\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Please click on `Validate` on the top, once you are done with the exercises." ] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "Julia 1.4", "language": "julia", "name": "julia-1.3" }, "language": "Julia", "language_info": { "file_extension": ".jl", "mimetype": "application/julia", "name": "julia", "version": "1.4.0" }, "toc": { "nav_menu": { "height": "119px", "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": 4 }