12

Adding static type checking to Julia in 100 lines of code

 4 years ago
source link: https://www.tuicool.com/articles/IZ3I3qM
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
["^ ","~:view-data",["^ ","~:article-settings",null,"~:article-access?",false,"~:preview?",false,"~:article",["^ ","~:article/preview","QmfQzkQpKXRm8uv7PM9tZVfCJ7ENPHsSuqYHPau9BUAvDN","~:article/published-at","~m1560340964084","~:article/title","Adding static type checking to Julia in 100 lines of code","~:article/change",["^ ","~:nextjournal/id","~u5d00e9aa-07ed-4f64-8a45-e83095c070a9","~:preview/name","QmfQzkQpKXRm8uv7PM9tZVfCJ7ENPHsSuqYHPau9BUAvDN","~:change/inserted-at","~m1560340906578"],"~:article/published-change",["^ ","^9","~u5d00e9aa-07ed-4f64-8a45-e83095c070a9","^:","QmfQzkQpKXRm8uv7PM9tZVfCJ7ENPHsSuqYHPau9BUAvDN","^;","~m1560340906578"],"^9","~u02b0a6fe-ceec-4239-bb2e-58e95fed035b","~:article/name","adding-static-type-checking-to-julia-in-100-lines-of-code","~:article/view-token","Sg3bpQ286A31kafezJrokc","~:article/profile",["^ ","^9","~u02b0a6ed-1a92-4dde-ab47-ac0417ab00fb","~:profile/name","Jonathan Bieler","~:profile/handle","jbieler","~:profile/avatar","","~:profile/private-access?",false],"~:article/remix-of",["^ ","^8",["^ ","^9","~u5cc9e9a1-3d1d-4d01-810e-4e947c1cfdd7","^:","QmZEZnHVUtd9XUMDEsTLAK2hm2yrGqpmwtvfseLTXgdetJ","^;","~m1556736417267"],"^?",["^ ","^9","~u59cd647c-ea83-4e5a-8188-cf43e36ee1e4","^@","Nextjournal","^A","nextjournal","~:profile/storage",["^ ","~:storage/bucket","nextjournal-customer-nextjournal","~:storage/docker-registry","eu.gcr.io/nextjournal-customer-nextjournal/environment"],"^B","c38b3f84-9754-42a4-9893-4d1f4941f2e3","^C",true],"^9","~u5b3e4020-24de-4fa9-b4bd-0050a176c13c","^=","julia-template","^6","~m1556742779622","^<",["^ ","^9","~u5cc9e9a1-3d1d-4d01-810e-4e947c1cfdd7","^:","QmZEZnHVUtd9XUMDEsTLAK2hm2yrGqpmwtvfseLTXgdetJ","^;","~m1556736417267"]]],"~:article-contents",["^ ","^4",["^ ","~:root","3950cb96-1122-4d9f-8b74-7168fb77849a","~:nodes",["^ ","633e5c54-d3a7-4597-9409-cc52b11f8870",["^ ","~:id","633e5c54-d3a7-4597-9409-cc52b11f8870","~:kind","paragraph","~:content","In order to enumerate every combination of concret types we can use Iterators.product :"],"eb4fb744-e6d7-4f3d-8adb-859ac9c067cc",["^ ","^N","using MLStyle\n\nmatch_signature(ast) = @match ast begin\n quote\n $(::LineNumberNode)\n [$body for $var=$range]\n end => body\nend\n\nmatch_signature( quote [i^2 for i=1:10] end )","~:refs",["~#list",[]],"~:output-log-lines",["^ ","~:stdout",0],"~:language","julia","^L","eb4fb744-e6d7-4f3d-8adb-859ac9c067cc","~:compute-ref","~u8b111559-439f-4f51-85de-0b6d5f9cf85d","~:runtime",["^V","90436991-2923-44bb-b0d1-f235aec846ef"],"^M","code","~:locked?",false,"~:outputs",["^ ","~_",["^ ","text/plain",["^ ","~:value",":(i ^ 2)"]]],"~:error",null,"~:exec-duration",17956],"1f2b4103-54ef-4d24-a079-b4ddda2adde9",["^ ","^N","match_type_annotation(ast) = @match ast begin :($(arg)::$(ann)) => ann end\nmatch_type_annotation.(sig.args) ","^P",["^Q",[]],"^R",["^ ","^S",0],"^T","julia","^L","1f2b4103-54ef-4d24-a079-b4ddda2adde9","^U","~uf983ff27-57ea-42a3-a386-b29893affa20","^V",["^V","90436991-2923-44bb-b0d1-f235aec846ef"],"^M","code","^X",["^ ","~_",["^ ","^Y",["^ ","^Z","2-element Array{Symbol,1}:\n :Int8 \n :Int16"]]],"^[",null,"^10",1501],"12eea290-19ca-455b-a9fe-84af5802ea45",["^ ","^L","12eea290-19ca-455b-a9fe-84af5802ea45","^M","paragraph","^N","Note that Julia adds line numbers in its AST, so we also need to include a line number node in the pattern."],"4e0e2819-4d18-492d-b260-50fec8769db6",["^ ","^L","4e0e2819-4d18-492d-b260-50fec8769db6","^M","formula","^N","body for "],"9c465dbf-5ea9-41c3-b90f-b1eb7a906ab3",["^ ","^L","9c465dbf-5ea9-41c3-b90f-b1eb7a906ab3","^M","paragraph","^N","We also need a method that will take and abstract type annotation ( [ : T,:K] ) and replace it with the corresponding concrete subtypes:"],"710d34d0-6299-4e3c-a3ac-9e4f00e8e983",["^ ","^N","function report_type_check(predicate,inferred,fun,types)\n passed = predicate(inferred)\n sig = join(types,\",\")\n status = passed ? \"SUCCESS\" : \"FAILURE\"\n println( \"$status: $(fun)($sig) \\t → $inferred\" ) \nend","^P",["^Q",[]],"^R",["^ ","^S",0],"^T","julia","^L","710d34d0-6299-4e3c-a3ac-9e4f00e8e983","^U","~ud526191b-4d89-4af2-9af0-de5e0a7c4755","^V",["^V","90436991-2923-44bb-b0d1-f235aec846ef"],"^M","code","^X",["^ ","~_",["^ ","^Y",["^ ","^Z","report_type_check (generic function with 1 method)"]]],"^[",null,"^10",869],"4d8dd6bd-7a79-4ab4-b658-baa7c4b253cf",["^ ","^N","collect(Iterators.product([Bool,BigInt],[Bool,BigInt]))","^P",["^Q",[]],"^R",["^ ","^S",0],"^T","julia","^L","4d8dd6bd-7a79-4ab4-b658-baa7c4b253cf","^U","~udeb7bce8-001c-4f88-bb21-0c8de9d9179f","^V",["^V","90436991-2923-44bb-b0d1-f235aec846ef"],"^M","code","^X",["^ ","~_",["^ ","^Y",["^ ","^Z","2×2 Array{Tuple{DataType,DataType},2}:\n (Bool, Bool) (Bool, BigInt) \n (BigInt, Bool) (BigInt, BigInt)"]]],"^[",null,"^10",1726],"f86136d2-fc19-4aed-8164-f53daa83d2de",["^ ","^L","f86136d2-fc19-4aed-8164-f53daa83d2de","^M","numbered-list","^N","Write an inferred method that return the inferred return type"],"b2c2e801-4f9f-4837-b811-a0ccc7dd4bfd",["^ ","^L","b2c2e801-4f9f-4837-b811-a0ccc7dd4bfd","^M","paragraph","^N","Here idx contains the indices of where the abstract types annotations occurs in the method signature (they can occur at several positions, e.g. f(x::T, y::T, z::K) where {T,K} then idx = [[1,2],[3]] )."],"724f076c-b41e-483d-963d-89ad82c88ce1",["^ ","^L","724f076c-b41e-483d-963d-89ad82c88ce1","^M","paragraph","^N","Into:"],"c2a7bf98-e4f1-4c7c-bdd6-730640273fd5",["^ ","^L","c2a7bf98-e4f1-4c7c-bdd6-730640273fd5","^M","paragraph","^N","Given that Julia is dynamic (types can be added at any time) and generic by default (meaning a huge number of types would need to be checked in many cases) this kind of checks might not be very useful in practice. However, it's a good excuse to explore Julia's meta-programming and introspection tools."],"46fbc992-84b4-44f5-9734-498921e86f94",["^ ","^L","46fbc992-84b4-44f5-9734-498921e86f94","^M","paragraph","^N","We can now write a macro that will perform type checking:"],"0df52233-9ac4-4137-83e8-d99d59cb087a",["^ ","^L","0df52233-9ac4-4137-83e8-d99d59cb087a","^M","section","^N",["^Q",["961363c8-c063-47d8-8c22-65f18faec30e","2668304b-f3df-40ad-9386-c07c1776e1f0","957d7e78-337d-49e2-a3eb-dd5cf31cebfd"]],"~:title","Limitations","~:sections",["^Q",[]]],"2668304b-f3df-40ad-9386-c07c1776e1f0",["^ ","^L","2668304b-f3df-40ad-9386-c07c1776e1f0","^M","paragraph","^N","The sheer number type combinations to check can also become an issue, for example there's 16 concrete subtype of Number in Base, so a function taking two numbers will generate 256 checks. There's 8998 concrete types in Base, so testing a function that has a single parameter annotated as Any would take that many checks, which is not very realistic."],"95953863-f7b1-40ef-ae45-69d9264e3b38",["^ ","^N","match_subtype(ast) = @match ast begin :($(sub)<:$(sup)) => (sub=sub,sup=sup) end\nmatch_subtype.(sig.cov)","^P",["^Q",[]],"^R",["^ ","^S",0],"^T","julia","^L","95953863-f7b1-40ef-ae45-69d9264e3b38","^U","~u9284cb6e-ef0e-4a76-a641-bd58572bcb66","^V",["^V","90436991-2923-44bb-b0d1-f235aec846ef"],"^M","code","^X",["^ ","~_",["^ ","^Y",["^ ","^Z","2-element Array{NamedTuple{(:sub, :sup),Tuple{Symbol,Symbol}},1}:\n (sub = :T, sup = :Integer)\n (sub = :K, sup = :Integer)"]]],"^[",null,"^10",1532],"d102df09-2246-410f-b1ef-2ece7dff8cc3",["^ ","^L","d102df09-2246-410f-b1ef-2ece7dff8cc3","^M","formula","^N","name("],"d3620fdb-b61b-48f9-baef-150cd45a8fa1",["^ ","^N","import InteractiveUtils.subtypes\n\nfunction allsubtypes(T::Type)\n isconcretetype(T) && return [T]\n t = Type[]\n map(K->allsubtypes(K,t), subtypes(T))\n t\nend\nallsubtypes(T,t) = isconcretetype(T) ? push!(t,T) : map(K->allsubtypes(K,t), subtypes(T))\nallsubtypes(T::Symbol) = allsubtypes(Core.eval(Main,T))#convert Symbol to Type\n\nallsubtypes(Integer)","^P",["^Q",[]],"^R",["^ ","^S",0],"^T","julia","^L","d3620fdb-b61b-48f9-baef-150cd45a8fa1","^U","~ua17ef8da-e7d9-438e-9a9e-734d951c2aa9","^V",["^V","90436991-2923-44bb-b0d1-f235aec846ef"],"^M","code","^X",["^ ","~_",["^ ","^Y",["^ ","^Z","12-element Array{Type,1}:\n Bool \n BigInt \n Int128 \n Int16 \n Int32 \n Int64 \n Int8 \n UInt128\n UInt16 \n UInt32 \n UInt64 \n UInt8 "]]],"^[",null,"^10",2427],"37c70fe3-99d2-47ba-8e8f-f185a666a470",["^ ","^L","37c70fe3-99d2-47ba-8e8f-f185a666a470","^M","paragraph","^N","Here we use @macroexpand to see the code built by the macro. It first defines the function itself, and then adds a type check with the declared input and output types. We can now test that our macro work correctly:"],"49563616-b72b-433e-986c-346d992290f3",["^ ","^L","49563616-b72b-433e-986c-346d992290f3","^M","paragraph","^N","We will first deal with the simplest case possible (a method annotated with concrete types) and then generalize to parametric methods."],"3950cb96-1122-4d9f-8b74-7168fb77849a",["^ ","^L","3950cb96-1122-4d9f-8b74-7168fb77849a","^1=","Adding static type checking to Julia in 100 lines of code","^M","section","~:version",2,"^N",["^Q",["f5c4f6ad-ad83-47ed-a20e-350d02db78b8","c2a7bf98-e4f1-4c7c-bdd6-730640273fd5","49563616-b72b-433e-986c-346d992290f3"]],"^1>",["^Q",["0fcb6931-f25d-45f7-89b0-1a443dcc38db","5f5b9419-ec65-48d5-afaf-cc1f547c3549","0df52233-9ac4-4137-83e8-d99d59cb087a","f8afd193-d1b4-418c-82eb-5c2d23f4c107"]]],"dd429f06-55aa-4663-8e3d-df9194be6be1",["^ ","^L","dd429f06-55aa-4663-8e3d-df9194be6be1","^M","paragraph","^N","And finally we can update our checked macro:"],"0c533af5-ba48-432d-bc2b-87cfe7b02c51",["^ ","^L","0c533af5-ba48-432d-bc2b-87cfe7b02c51","^M","paragraph","^N","In order to do this we need to:","~:inlines",["aa983285-fe9d-4731-bcc5-d163d797558c","bdfca447-49ff-41e8-a03b-a77891c3e570","14080287-9b9a-46f6-b679-6bac4207fd81","c268c252-4ac5-4863-9afd-45d72bc5bbcf","22a57914-2605-4d58-a01a-078bb0efc7c5","f750ad4c-c520-4232-aaa7-9f6bf6409c07","d102df09-2246-410f-b1ef-2ece7dff8cc3","bf3b040b-08c6-474f-994d-35624479ae7c"]],"96a8e2b0-cee7-4370-9801-d67e8c4b7e39",["^ ","^N","using Test\n@test_throws AssertionError @checked Int8 function add(x::Int8,y::Int16)\n x+y\nend","^P",["^Q",[]],"^R",["^ ","^S",0],"^T","julia","^L","96a8e2b0-cee7-4370-9801-d67e8c4b7e39","^U","~u6976c459-312c-49b2-bf2c-95d709316790","^V",["^V","90436991-2923-44bb-b0d1-f235aec846ef"],"^M","code","^X",["^ ","~_",["^ ","^Y",["^ ","^Z","Test Passed\n Thrown: AssertionError"]]],"^[",null,"^10",1033],"e054f754-a441-4c7f-897b-dd407b7c9150",["^ ","^L","e054f754-a441-4c7f-897b-dd407b7c9150","^M","paragraph","^N","For 1. we will use the MLStyle package which adds pattern matching for ASTs, making this task very easy."],"1fff957f-6ba1-42c4-9762-9314b9ff2af0",["^ ","^L","1fff957f-6ba1-42c4-9762-9314b9ff2af0","^M","paragraph","^N","We will deal with the simplest case to begin with. We want to build a macro @checked that will take a return type and a function definition as input and add a test that will check that the inferred return type matches the declared one after the function definition. In short, transforms the code:"],"37971c6e-c940-45c0-bbd2-291a188e0d88",["^ ","^L","37971c6e-c940-45c0-bbd2-291a188e0d88","^M","paragraph","^N","In order to test every combination of parameters under the constrains above we need to enumerate all concret subtypes of Integer . This can be done by recursively calling InteractiveUtils.subtypes :"],"b707eb6d-5178-4113-8171-90f88db4f5cd",["^ ","^L","b707eb6d-5178-4113-8171-90f88db4f5cd","^M","code-listing","^N","@checked I -> I == T function add(x::T, y::K) where {T<:Integer, K<:Integer} \n x + y\nend","^T","julia"],"87ef8596-995f-427f-9324-38f37684934f",["^ ","^L","87ef8596-995f-427f-9324-38f37684934f","^M","formula","^N","body for "],"b8c61413-2a6c-4450-9c63-2475007b6a49",["^ ","^L","b8c61413-2a6c-4450-9c63-2475007b6a49","^M","paragraph","^N","We can now put all this together in a method that will generate the expression that performs the check for every combination of concrete parameters:"],"0e4c32c1-8289-49d8-be3e-796a21cd4e95",["^ ","^L","0e4c32c1-8289-49d8-be3e-796a21cd4e95","^M","paragraph","^N","Similarly we define a method to extract type annotations ( x::T ):"],"5f5b9419-ec65-48d5-afaf-cc1f547c3549",["^ ","^L","5f5b9419-ec65-48d5-afaf-cc1f547c3549","^M","section","^N",["^Q",["fd1ba614-bdf6-4692-b2c3-5a3166fe316f","b707eb6d-5178-4113-8171-90f88db4f5cd","ff7c466c-53be-4ad2-ba6a-1b951587aff0","c223156e-7e6d-4a54-93d2-b51ade697dde","7588fb7a-6796-492a-b7e1-621a36fc6b0f","95953863-f7b1-40ef-ae45-69d9264e3b38","37971c6e-c940-45c0-bbd2-291a188e0d88","d3620fdb-b61b-48f9-baef-150cd45a8fa1","633e5c54-d3a7-4597-9409-cc52b11f8870","4d8dd6bd-7a79-4ab4-b658-baa7c4b253cf","9c465dbf-5ea9-41c3-b90f-b1eb7a906ab3","19566cc9-4d5c-4872-bba7-2d735eb3039c","b2c2e801-4f9f-4837-b811-a0ccc7dd4bfd","1ea79872-2a12-41f7-beda-7c39ff731296","0bb92da9-7c7a-485a-b000-119ffa9e2f84","b8c61413-2a6c-4450-9c63-2475007b6a49","b11f3829-3f88-4559-9343-83a49ea869fa","47ac3dbb-8509-46cd-8536-13e2cea803fe","710d34d0-6299-4e3c-a3ac-9e4f00e8e983","dd429f06-55aa-4663-8e3d-df9194be6be1","904672f0-78c4-4dd5-8f63-39f95c6381f2","867b89eb-d388-4a35-b865-bdc4aee7f73d"]],"^1=","Parametric methods"],"a93cf99a-5721-4f1d-ab10-015c86b89d1d",["^ ","^L","a93cf99a-5721-4f1d-ab10-015c86b89d1d","^M","code-listing","^N","using MLStyle\nimport InteractiveUtils.subtypes\n\nmatch_signature(ast) = @match ast begin\n quote\n $(::LineNumberNode)\n function $name($(args...)) where {$(cov...)}; $(block...) end\n end ||\n quote\n $(::LineNumberNode)\n function $name($(args...)); $(block...) end\n end && Do(cov = []) =>\n (name = name,\n args = args,\n cov = cov)\nend\n\nmatch_type_annotation(ast) = @match ast begin :($(arg)::$(ann)) => ann end\nmatch_subtype(ast) = @match ast begin :($(sub)<:$(sup)) => (sub=sub,sup=sup) end\n\nfunction inferred(f,args)\n codeinfo = Base.code_typed(f,args)[1]\n codeinfo.second\nend\n\nfunction allsubtypes(T::Type)\n isconcretetype(T) && return [T]\n t = Type[]\n map(K->allsubtypes(K,t), subtypes(T))\n t\nend\nallsubtypes(T,t) = isconcretetype(T) ? push!(t,T) : map(K->allsubtypes(K,t), subtypes(T))\nallsubtypes(T::Symbol) = allsubtypes(Core.eval(Main,T))#convert Symbol to Type\n\nfunction concretize!(args, concrete_args, idx)\n for (j,K) in enumerate(concrete_args)\n for i in idx[j]\n args[i] = Symbol(\"$K\")\n end\n end\nend\n\nfunction ast_replace!(ex::Expr, pair::Pair)\n for i=1:length(ex.args)\n if ex.args[i] == pair.first\n ex.args[i] = pair.second\n else \n ast_replace!(ex.args[i], pair) \n end\n end\n ex\nend \nast_replace!(ex::Symbol, pair) = nothing \nast_replace!(ex::LineNumberNode, pair) = nothing\n\nfunction check_types(fun, predicate, args, covs)\n \t#setup\n out = Expr[]\n covs = match_subtype.(covs)\n covs_symbols = [c.sub for c in covs]\n idx = [findall(args .== c) for c in covs_symbols]\n \n for p in Iterators.product([allsubtypes(c.sup) for c in covs]...) \n \n \t\t#build concrete arguments\n concrete_args = copy(args) \n\t\t\t concretize!(concrete_args, p, idx)\n \n #build concrete predicate\n concrete_predicate = copy(predicate)\n for pairs in [Pair(c,Symbol(t)) for (c,t) in zip(covs_symbols,p)]\n ast_replace!(concrete_predicate,pairs)\n end\n \n \t\t#build final expression\n t = :( Tuple{$(concrete_args...)} )\n I = :( inferred($fun,$t) )\n push!(out, quote \n report_type_check($concrete_predicate,$I,$fun,$concrete_args)\n end)\n end\n out\nend\n\nfunction report_type_check(predicate,inferred,fun,types)\n passed = predicate(inferred)\n sig = join(types,\",\")\n status = passed ? \"SUCCESS\" : \"FAILURE\"\n println( \"$status: $(fun)($sig) \\t → $inferred\" ) \nend\n\nmacro checked(out,f)\n sig = match_signature(quote $f end)\n args = match_type_annotation.(sig.args)\n \n checks = check_types(Symbol(sig.name),out,args,sig.cov)\n \n esc(quote\n $f\n $(checks...)\n end)\nend\n\n@checked O -> O == promote_type(T,K) function add(x::T, y::K) where {T<: Integer, K<: Integer} \n x + y\nend","^T","julia","^P",["^Q",[]],"^[",null],"eaecc731-3c56-416f-95bd-105b3cbbc584",["^ ","^L","eaecc731-3c56-416f-95bd-105b3cbbc584","^M","code-listing","^N","@checked Int16 function add(x::Int8, y::Int16) \n x+y\nend","^T","julia"],"bdfca447-49ff-41e8-a03b-a77891c3e570",["^ ","^L","bdfca447-49ff-41e8-a03b-a77891c3e570","^M","formula","^N","name("],"4a36f696-56d0-4fd6-a7a4-e9f1b86be6fa",["^ ","^L","4a36f696-56d0-4fd6-a7a4-e9f1b86be6fa","^M","numbered-list","^N","Generate the final expression"],"0bb92da9-7c7a-485a-b000-119ffa9e2f84",["^ ","^N","#find all pair.first in expression and replace them by pair.second\nfunction ast_replace!(ex::Expr, pair::Pair)\n for i=1:length(ex.args)\n if ex.args[i] == pair.first\n ex.args[i] = pair.second\n else \n ast_replace!(ex.args[i], pair) \n end\n end\n ex\nend \nast_replace!(ex::Symbol, pair) = nothing \nast_replace!(ex::LineNumberNode, pair) = nothing\n\nast_replace!(:(I -> I == T), :T => Int8)","^P",["^Q",[]],"^R",["^ ","^S",0],"^T","julia","^L","0bb92da9-7c7a-485a-b000-119ffa9e2f84","^U","~ub9056057-e51b-4fe6-8f20-ed1350746c6f","^V",["^V","90436991-2923-44bb-b0d1-f235aec846ef"],"^M","code","^X",["^ ","~_",["^ ","^Y",["^ ","^Z",":(I->begin\n #= cell:15 =#\n I == Int8\n end)"]]],"^[",null,"^10",631],"ff7c466c-53be-4ad2-ba6a-1b951587aff0",["^ ","^L","ff7c466c-53be-4ad2-ba6a-1b951587aff0","^M","paragraph","^N","We first generalise our match_signature method to handle the parametric case, using an OR clause to handle functions without where :"],"98119e54-bcbd-4b5c-b652-a056273095bd",["^ ","^L","98119e54-bcbd-4b5c-b652-a056273095bd","^M","paragraph","^N","If the inferred type is incorrect the macro will throw an AssertionError when the function is defined:"],"bf0615fa-5c7e-4d67-8868-eb9285add331",["^ ","^N","match_signature(ast) = @match ast begin\n quote\n $(::LineNumberNode)\n function $name($(args...)); $(block...) end\n end =>\n (name = name,\n args = args)\nend\n\nsig = match_signature(quote \n function add(x::Int8,y::Int16)\n x+y\n end\nend)","^P",["^Q",[]],"^R",["^ ","^S",0],"^T","julia","^L","bf0615fa-5c7e-4d67-8868-eb9285add331","^U","~u930567cb-7f9d-424f-9dc7-a019ced9ccd2","^V",["^V","90436991-2923-44bb-b0d1-f235aec846ef"],"^M","code","^X",["^ ","~_",["^ ","^Y",["^ ","^Z","(name = :add, args = Any[:(x::Int8), :(y::Int16)])"]]],"^[",null,"^10",1706],"be721a35-b3b0-4dac-9b00-f46605ef5933",["^ ","^L","be721a35-b3b0-4dac-9b00-f46605ef5933","^M","numbered-list","^N","Extract the type annotation ( Int8 , Int16 ) from the Abstract Syntax Tree ( AST )"],"ee583225-5f5e-4581-bcb3-95e53cb0a8b1",["^ ","^L","ee583225-5f5e-4581-bcb3-95e53cb0a8b1","^M","paragraph","^N","For example if we want to extract the body of a comprehension [i^2 for i=1:10] using MLStyle's @match macro, we simply need to replace each element by wildcards [$body for $var=$range] :","^1I",["87ef8596-995f-427f-9324-38f37684934f","4e0e2819-4d18-492d-b260-50fec8769db6","693c85fe-d188-4f42-8b4a-15e4a70796f7"]],"b11f3829-3f88-4559-9343-83a49ea869fa",["^ ","^N","function check_types(fun, predicate, args, covs)\n \t#setup\n out = Expr[]\n covs = match_subtype.(covs)\n covs_symbols = [c.sub for c in covs]\n idx = [findall(args .== c) for c in covs_symbols]\n \n for p in Iterators.product([allsubtypes(c.sup) for c in covs]...) \n \n \t\t#build concrete arguments\n concrete_args = copy(args) \n\t\t\t concretize!(concrete_args, p, idx)\n \n #build concrete predicate\n concrete_predicate = copy(predicate)\n for pairs in [Pair(c,Symbol(t)) for (c,t) in zip(covs_symbols,p)]\n ast_replace!(concrete_predicate,pairs)\n end\n \n \t\t#build final expression\n t = :( Tuple{$(concrete_args...)} )\n I = :( inferred($fun,$t) )\n push!(out, quote \n report_type_check($concrete_predicate,$I,$fun,$concrete_args)\n end)\n end\n out\nend\n\nfun, args = :add, [:T,:K]\ncovs = Any[:(T<: Integer), :(K<: Integer)]\nout_type = :(O -> O == T)\n\ncheck_types(fun, out_type, args, covs)[1:2]","^P",["^Q",[]],"^R",["^ ","^S",0],"^T","julia","^L","b11f3829-3f88-4559-9343-83a49ea869fa","^U","~u4bdbd707-af34-4942-b9d1-5805b7772fc0","^V",["^V","90436991-2923-44bb-b0d1-f235aec846ef"],"^M","code","^X",["^ ","~_",["^ ","^Y",["^ ","^Z","2-element Array{Expr,1}:\n quote\n #= cell:24 =#\n report_type_check((O->begin\n #= cell:32 =#\n O == Bool\n end), inferred(add, Tuple{Bool, Bool}), add, Symbol[:Bool, :Bool])\nend \n quote\n #= cell:24 =#\n report_type_check((O->begin\n #= cell:32 =#\n O == BigInt\n end), inferred(add, Tuple{BigInt, Bool}), add, Symbol[:BigInt, :Bool])\nend"]]],"^[",null,"^10",2471],"5ffff7e0-782a-4a9e-9184-2fe5cfd71336",["^ ","^L","5ffff7e0-782a-4a9e-9184-2fe5cfd71336","^M","paragraph","^N","We can simply run type inference using Base.code_typed :"],"90436991-2923-44bb-b0d1-f235aec846ef",["^ ","~:runtime/inherited-environment-variables",["^Q",[["^ ","~:name","NEXTJOURNAL_RUNTIME_SERVICE_URL","^Z","https://nextjournal.com/runner/CGiAfcRNpELDxJT9SCAM4H/runtime/39e3f06d-60bf-4003-ae1a-62e835085aef"],["^ ","^25","GKSwstype","^Z","svg"],["^ ","^25","DISPLAY","^Z",":0"],["^ ","^25","NVIDIA_VISIBLE_DEVICES","^Z","all"],["^ ","^25","NVIDIA_DRIVER_CAPABILITIES","^Z","all"],["^ ","^25","PATH","^Z","/usr/local/julia/bin:/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],["^ ","^25","JULIA_PATH","^Z","/usr/local/julia"],["^ ","^25","JULIA_VERSION","^Z","1.1.0"],["^ ","^25","NEXTJOURNAL_MOUNT_CUDA","^Z","9.2-cudnn7-devel-ubuntu18.04"],["^ ","^25","BASH_ENV","^Z","/.bash_profile"],["^ ","^25","LC_ALL","^Z","en_US.UTF-8"],["^ ","^25","LANGUAGE","^Z","en_US.en"],["^ ","^25","LANG","^Z","en_US.UTF-8"]]],"^25","MLStyle","~:docker/environment-image","docker.nextjournal.com/environment@sha256:446cd9dee9803d392f31ebfb5f871cdb8f939b8324aed063a52582c2e979fb1c","~:type","~:nextjournal","~:environment?",true,"^T","julia","^L","90436991-2923-44bb-b0d1-f235aec846ef","^U","~u13adc3c0-8cf2-11e9-997b-7177f3c2b203","^M","runtime","~:changed?",true,"^[",null,"~:environment",["^2;",["^ ","~:article/nextjournal.id","~u5b460d39-8c57-43a6-8b13-e217642b0146","~:change/nextjournal.id","~u5cc9b90b-0bd4-4e38-bb95-834151b2dc86","~:node/id","39e3f06d-60bf-4003-ae1a-62e835085aef"]],"~:runtime/features",["~#set",["complete","doc"]],"~:diff","C /root/.julia/registries/General/L (2 deleted, 74 changed, 9 added)\nC /root/.julia/registries/General/Y/YaoBlocks/Compat.toml (1 changed)\nC /root/.julia/registries/General/V (1 deleted, 20 changed, 4 added)\nC /root/.julia/registries/General/X/XLSX/Compat.toml (1 changed)\nC /root/.julia/registries/General/K (1 deleted, 22 changed, 4 added)\nC /root/.julia/registries/General/T (8 deleted, 80 changed, 32 added)\nC /root/.julia/registries/General/X/XPA/Compat.toml (1 changed)\nC /root/.julia/registries/General/B (4 deleted, 50 changed, 16 added)\nC /root/.julia/registries/General/A (5 deleted, 66 changed, 21 added)\nC /root/.julia/registries/General/G (6 deleted, 71 changed, 25 added)\nC /.nextjournal (1 deleted, 2 added)\nC /root/.julia/registries/General/Q (2 deleted, 24 changed, 8 added)\nC /root/.julia/registries/General/Registry.toml (1 changed)\nC /root/.julia/registries/General/U (1 deleted, 15 changed, 4 added)\nC /root/.julia/registries/General/X/XLSX/Versions.toml (1 changed)\nC /root/.julia/registries/General/O (2 deleted, 36 changed, 9 added)\nC /root/.julia/registries/General/X/XSim/Compat.toml (1 changed)\nC /root/.julia/registries/General/Y/YaoBlocks/Versions.toml (1 changed)\nC /root/.julia/registries/General/.git (11 changed, 2 added)\nC /root/.julia/registries/General/Z (1 deleted, 7 changed, 4 added)\nC /root/.julia/registries/General/M (7 deleted, 104 changed, 28 added)\nC /root/.julia/registries/General/F (3 deleted, 51 changed, 12 added)\nC /root/.julia/registries/General/H (2 deleted, 26 changed, 8 added)\nC /root/.julia/registries/General/J (2 deleted, 30 changed, 8 added)\nC /root/.julia/logs/manifest_usage.toml (1 changed)\nC /root/.julia/registries/General/N (4 deleted, 37 changed, 16 added)\nC /root/.julia/registries/General/W (2 deleted, 18 changed, 8 added)\nC /root/.julia/environments/v1.1/Project.toml (1 changed)\nC /root/.julia/registries/General/P (11 deleted, 98 changed, 44 added)\nC /root/.julia/registries/General/Y/Yao/Compat.toml (1 changed)\nC /root/.julia/registries/General/Y/Yao/Versions.toml (1 changed)\nC /root/.julia/registries/General/I (2 deleted, 74 changed, 8 added)\nC /root/.julia/registries/General/Y/YaoArrayRegister/Versions.toml (1 changed)\nC /root/.julia/registries/General/E (6 deleted, 29 changed, 24 added)\nC /root/.julia/registries/General/S (7 deleted, 137 changed, 29 added)\nC /tmp (1 changed)\nC /root/.julia/environments/v1.1/Manifest.toml (1 changed)\nC /root/.julia/registries/General/R (2 deleted, 46 changed, 8 added)\nC /root/.julia/registries/General/D (6 deleted, 114 changed, 24 added)\nC /root/.julia/packages (37 deleted, 34 changed, 7206 added)\nC /root/.julia/registries/General/Y/YaoArrayRegister/Compat.toml (1 changed)\nC /root/.julia/compiled/v1.1 (1 deleted, 1 changed, 1 added)\nC /root/.julia/registries/General/C (5 deleted, 106 changed, 20 added)"],"1ea79872-2a12-41f7-beda-7c39ff731296",["^ ","^L","1ea79872-2a12-41f7-beda-7c39ff731296","^M","paragraph","^N","We need to perform the same operation on our predicate (e.g. I -> I == T ), which can be an arbitrary expression. We will thus write a generic method that can replace a symbol by another in an expression:"],"c268c252-4ac5-4863-9afd-45d72bc5bbcf",["^ ","^L","c268c252-4ac5-4863-9afd-45d72bc5bbcf","^M","formula","^N","name("],"957d7e78-337d-49e2-a3eb-dd5cf31cebfd",["^ ","^L","957d7e78-337d-49e2-a3eb-dd5cf31cebfd","^M","paragraph","^N","For that reason this kind of tool might be better tailored for automatic tests, ran only during testing for some specific cases."],"89d627b4-2acb-4fc0-9feb-2d791ab6722a",["^ ","^L","89d627b4-2acb-4fc0-9feb-2d791ab6722a","^M","paragraph","^N","When adding two integers of different type, the result gets automaticallypromoted(here to Int16 ). The promoted type can be computed using promote_type(T,K) ."],"f750ad4c-c520-4232-aaa7-9f6bf6409c07",["^ ","^L","f750ad4c-c520-4232-aaa7-9f6bf6409c07","^M","formula","^N","name("],"14080287-9b9a-46f6-b679-6bac4207fd81",["^ ","^L","14080287-9b9a-46f6-b679-6bac4207fd81","^M","formula","^N","name("],"c223156e-7e6d-4a54-93d2-b51ade697dde",["^ ","^N","match_signature(ast) = @match ast begin\n quote\n $(::LineNumberNode)\n function $name($(args...)) where {$(cov...)}; $(block...) end\n end ||\n quote\n $(::LineNumberNode)\n function $name($(args...)); $(block...) end\n end && Do(cov = []) =>\n (name = name,\n args = args,\n cov = cov)\nend\n\nsig = match_signature(quote \n function add(x::T,y::K) where {T<:Integer, K<:Integer}\n x+y\n end\nend)","^P",["^Q",[]],"^R",["^ ","^S",0],"^T","julia","^L","c223156e-7e6d-4a54-93d2-b51ade697dde","^U","~ueab8b51a-578e-4bad-8e11-5986ae34b9f9","^V",["^V","90436991-2923-44bb-b0d1-f235aec846ef"],"^M","code","^X",["^ ","~_",["^ ","^Y",["^ ","^Z","(name = :add, args = Any[:(x::T), :(y::K)], cov = Any[:(T<: Integer), :(K<: Integer)])"]]],"^[",null,"^10",2112],"b04af5e2-9e00-43bd-9fda-9cf20e5dcce5",["^ ","^N","function inferred(f,args)\n codeinfo = Base.code_typed(f,args)[1]\n codeinfo.second\nend\n\n@assert inferred((x,y) -> x+y, Tuple{Int8,Int16}) == Int16","^P",["^Q",[]],"^R",["^ ","^S",0],"^T","julia","^L","b04af5e2-9e00-43bd-9fda-9cf20e5dcce5","^U","~u09926ded-f79c-43e7-b82a-64a32df4fe03","^V",["^V","90436991-2923-44bb-b0d1-f235aec846ef"],"^M","code","^X",["^ "],"^[",null,"^10",623],"f8afd193-d1b4-418c-82eb-5c2d23f4c107",["^ ","^L","f8afd193-d1b4-418c-82eb-5c2d23f4c107","^M","section","^N",["^Q",["a93cf99a-5721-4f1d-ab10-015c86b89d1d"]],"^1=","Final code"],"47ac3dbb-8509-46cd-8536-13e2cea803fe",["^ ","^L","47ac3dbb-8509-46cd-8536-13e2cea803fe","^M","paragraph","^N","In the generated code we call a helper function that will display the result of the type check instead of throwing an error:"],"904672f0-78c4-4dd5-8f63-39f95c6381f2",["^ ","~:stdout-collapsed?",false,"^N","macro checked(out,f)\n sig = match_signature(quote $f end)\n args = match_type_annotation.(sig.args)\n \n checks = check_types(Symbol(sig.name),out,args,sig.cov)\n \n esc(quote\n $f\n $(checks...)\n end)\nend\n\n@checked O -> O == promote_type(T,K) function add(x::T, y::K) where {T<: Integer, K<: Integer} \n x + y\nend","^P",["^Q",[]],"^R",["^ ","^S",144],"^T","julia","^L","904672f0-78c4-4dd5-8f63-39f95c6381f2","^U","~u8cf8ffde-81f5-461d-8c30-6830e1e89c3d","^V",["^V","90436991-2923-44bb-b0d1-f235aec846ef"],"^M","code","^X",["^ "],"^[",null,"^10",3184],"fd1ba614-bdf6-4692-b2c3-5a3166fe316f",["^ ","^L","fd1ba614-bdf6-4692-b2c3-5a3166fe316f","^M","paragraph","^N","Next we will consider a simple parametric case, we will also replace the output type annotation by a predicate on the inferred type I to allow for more general (and useful) checks. For example in the following, the inferred type will be compared to the type of the first argument:"],"19566cc9-4d5c-4872-bba7-2d735eb3039c",["^ ","^N","function concretize!(args, concrete_args, idx)\n for (j,K) in enumerate(concrete_args)\n for i in idx[j]\n args[i] = Symbol(\"$K\")\n end\n end\nend\n\nargs = Symbol[:T, :K]\nconcrete_args = (Bool, Bool)\nidx = [[1], [2]]\n\nconcretize!(args, concrete_args, idx)\nargs","^P",["^Q",[]],"^R",["^ ","^S",0],"^T","julia","^L","19566cc9-4d5c-4872-bba7-2d735eb3039c","^U","~uec71d564-ebd4-411d-bc23-50db0c1a9acb","^V",["^V","90436991-2923-44bb-b0d1-f235aec846ef"],"^M","code","^X",["^ ","~_",["^ ","^Y",["^ ","^Z","2-element Array{Symbol,1}:\n :Bool\n :Bool"]]],"^[",null,"^10",642],"22a57914-2605-4d58-a01a-078bb0efc7c5",["^ ","^L","22a57914-2605-4d58-a01a-078bb0efc7c5","^M","formula","^N","name("],"aa983285-fe9d-4731-bcc5-d163d797558c",["^ ","^L","aa983285-fe9d-4731-bcc5-d163d797558c","^M","formula","^N","name("],"e88c50bc-3733-4fe1-960d-82e53ac947ff",["^ ","^N","macro checked(out,f)\n sig = match_signature(quote $f end)\n args = match_type_annotation.(sig.args)\n \n \tchecks = quote\n @assert inferred($(sig.name),Tuple{$(args...)}) == $out\n end\n \n esc(quote\n $f\n $checks\n end)\nend\n\n@macroexpand @checked Int16 function add(x::Int8,y::Int16)\n x+y\nend","^P",["^Q",[]],"^R",["^ ","^S",0],"^T","julia","^L","e88c50bc-3733-4fe1-960d-82e53ac947ff","^U","~uaf21f7bf-1709-458b-91f6-94a0c1413140","^V",["^V","90436991-2923-44bb-b0d1-f235aec846ef"],"^M","code","^X",["^ ","~_",["^ ","^Y",["^ ","^Z","quote\n #= cell:10 =#\n function add(x::Int8, y::Int16)\n #= cell:16 =#\n x + y\n end\n #= cell:11 =#\n begin\n #= cell:6 =#\n if inferred(add, Tuple{Int8, Int16}) == Int16\n nothing\n else\n (Base.throw)((Base.AssertionError)(\"inferred(add, Tuple{Int8, Int16}) == Int16\"))\n end\n end\nend"]]],"^[",null,"^10",800],"0fcb6931-f25d-45f7-89b0-1a443dcc38db",["^ ","^L","0fcb6931-f25d-45f7-89b0-1a443dcc38db","^M","section","^N",["^Q",["1fff957f-6ba1-42c4-9762-9314b9ff2af0","eaecc731-3c56-416f-95bd-105b3cbbc584","724f076c-b41e-483d-963d-89ad82c88ce1","be25a040-1fb7-4edb-b748-665de284d620","0c533af5-ba48-432d-bc2b-87cfe7b02c51","be721a35-b3b0-4dac-9b00-f46605ef5933","f86136d2-fc19-4aed-8164-f53daa83d2de","4a36f696-56d0-4fd6-a7a4-e9f1b86be6fa","e054f754-a441-4c7f-897b-dd407b7c9150","ee583225-5f5e-4581-bcb3-95e53cb0a8b1","eb4fb744-e6d7-4f3d-8adb-859ac9c067cc","12eea290-19ca-455b-a9fe-84af5802ea45","d86baffb-ba23-46ea-a0fb-b1b338e85f9b","bf0615fa-5c7e-4d67-8868-eb9285add331","0e4c32c1-8289-49d8-be3e-796a21cd4e95","1f2b4103-54ef-4d24-a079-b4ddda2adde9","5ffff7e0-782a-4a9e-9184-2fe5cfd71336","b04af5e2-9e00-43bd-9fda-9cf20e5dcce5","89d627b4-2acb-4fc0-9feb-2d791ab6722a","46fbc992-84b4-44f5-9734-498921e86f94","e88c50bc-3733-4fe1-960d-82e53ac947ff","37c70fe3-99d2-47ba-8e8f-f185a666a470","7ce7faf3-ed2c-42fd-9b89-e35df0ec35ca","98119e54-bcbd-4b5c-b652-a056273095bd","96a8e2b0-cee7-4370-9801-d67e8c4b7e39"]],"^1=","Non-parametric methods"],"f5c4f6ad-ad83-47ed-a20e-350d02db78b8",["^ ","^L","f5c4f6ad-ad83-47ed-a20e-350d02db78b8","^M","paragraph","^N","While inJuliatype annotations are optional, the type system is designed in such a way that the types of variables in a function can be inferred for given inputs. By taking advantage of this we can add checks before a function ever gets called that the return types are as expected."],"7ce7faf3-ed2c-42fd-9b89-e35df0ec35ca",["^ ","^N","@checked Int16 function add(x::Int8,y::Int16)\n x+y\nend","^P",["^Q",[]],"^R",["^ ","^S",0],"^T","julia","^L","7ce7faf3-ed2c-42fd-9b89-e35df0ec35ca","^U","~ua970c92c-5666-44bc-9e36-0c97473dab5f","^V",["^V","90436991-2923-44bb-b0d1-f235aec846ef"],"^M","code","^X",["^ "],"^[",null,"^10",365],"bf3b040b-08c6-474f-994d-35624479ae7c",["^ ","^L","bf3b040b-08c6-474f-994d-35624479ae7c","^M","formula","^N","name("],"be25a040-1fb7-4edb-b748-665de284d620",["^ ","^L","be25a040-1fb7-4edb-b748-665de284d620","^M","code-listing","^N","function add(x::Int8, y::Int16) \n x+y\nend\n@assert inferred(add, Tuple{Int8, Int16}) == Int16","^T","julia"],"d86baffb-ba23-46ea-a0fb-b1b338e85f9b",["^ ","^L","d86baffb-ba23-46ea-a0fb-b1b338e85f9b","^M","paragraph","^N","We can match our function signature in a similar fashion, returning the matched elements as a named tuple:"],"7588fb7a-6796-492a-b7e1-621a36fc6b0f",["^ ","^L","7588fb7a-6796-492a-b7e1-621a36fc6b0f","^M","paragraph","^N","We need a small method to extract subtype relations ( T <: Integer ):"],"961363c8-c063-47d8-8c22-65f18faec30e",["^ ","^L","961363c8-c063-47d8-8c22-65f18faec30e","^M","paragraph","^N","One limitation of the implementation above is that it doesn't handle parametric types (e.g. Vector{T} where T <: Number ), but it should be relatively easy to generalize it to handle such a case. More general ones however might get tricky (e.g. Array{T,N} where {T <: Union{Number, String}, N} )."],"867b89eb-d388-4a35-b865-bdc4aee7f73d",["^ ","^L","867b89eb-d388-4a35-b865-bdc4aee7f73d","^M","paragraph","^N","All tests succeeded but the first one ( add(Bool,Bool) → Int64) since adding two booleans returns an Int64 . This could be fixed by modifying a bit the predicate to handle this particular case. "],"693c85fe-d188-4f42-8b4a-15e4a70796f7",["^ ","^L","693c85fe-d188-4f42-8b4a-15e4a70796f7","^M","formula","^N","body for "]],"~:transclusions",["~#cmap",[["^ ","^2<","~u5b460d39-8c57-43a6-8b13-e217642b0146","^2>","39e3f06d-60bf-4003-ae1a-62e835085aef","^2=","~u5cc9b90b-0bd4-4e38-bb95-834151b2dc86"],["^ ","^24",["^Q",[["^ ","^25","PATH","^Z","/usr/local/julia/bin:/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],["^ ","^25","JULIA_PATH","^Z","/usr/local/julia"],["^ ","^25","JULIA_VERSION","^Z","1.1.0"],["^ ","^25","NVIDIA_VISIBLE_DEVICES","^Z","void"],["^ ","^25","NVIDIA_DRIVER_CAPABILITIES","^Z","all"],["^ ","^25","NEXTJOURNAL_MOUNT_CUDA","^Z","9.2-cudnn7-devel-ubuntu18.04"],["^ ","^25","BASH_ENV","^Z","/.bash_profile"],["^ ","^25","LC_ALL","^Z","en_US.UTF-8"],["^ ","^25","LANGUAGE","^Z","en_US.en"],["^ ","^25","LANG","^Z","en_US.UTF-8"]]],"^;","~m1556723979380","~:transclusion",["^ ","^2<","~u5b460d39-8c57-43a6-8b13-e217642b0146","^2=","~u5cc9b90b-0bd4-4e38-bb95-834151b2dc86","^2>","39e3f06d-60bf-4003-ae1a-62e835085aef"],"^P",["^Q",[]],"^25","Julia 1.1","^26","docker.nextjournal.com/environment@sha256:7364b67c497e26b009ccf1d18d498f348a78f963aaa6104025e752183497c182","^27","^28","~:runtime/mounts",[["^ ","~:src",["~:node","19d44bc0-0939-4eb5-ac36-14866b67c32b"],"~:dest","/root/.confg/makie/theme.jl"],["^ ","^35",["^36","ca5b85ad-0b86-4a32-967d-c7786432b8d1"],"^37","/root/.juliarc.jl"]],"^29",true,"^T","julia","^L","39e3f06d-60bf-4003-ae1a-62e835085aef","^U","~u9dc47170-6c19-11e9-bb97-370c9fe1cc55","^M","runtime","^2:",false,"^[",null,"^2;",["^2;","897fce0d-2441-42a4-849d-77247f9a724a"],"~:runtime/environment-variables",[["^ ","^25","GKSwstype","^Z","svg"],["^ ","^25","DISPLAY","^Z",":0"]],"~:resources",["^ ","~:machine-type","n1-standard-4","~:accelerator-type","nvidia-tesla-k80","~:accelerator-count",1],"^2A",""]]]],"^L",17592189472608,"^9","~u02b0a6fe-ceec-4239-bb2e-58e95fed035b"],"~:show?",true,"~:change-id","CV6nEG96D1L3hfE3oDsJqN","^1F","published","~:article-collection",["^Q",[]],"~:html","

Adding static type checking to Julia in 100 lines of code

While inJuliatype annotations are optional, the type system is designed in such a way that the types of variables in a function can be inferred for given inputs. By taking advantage of this we can add checks before a function ever gets called that the return types are as expected.

Given that Julia is dynamic (types can be added at any time) and generic by default (meaning a huge number of types would need to be checked in many cases) this kind of checks might not be very useful in practice. However, it's a good excuse to explore Julia's meta-programming and introspection tools.

We will first deal with the simplest case possible (a method annotated with concrete types) and then generalize to parametric methods.

Non-parametric methods

We will deal with the simplest case to begin with. We want to build a macro @checked that will take a return type and a function definition as input and add a test that will check that the inferred return type matches the declared one after the function definition. In short, transforms the code:

@checked Int16 function add(x::Int8,
 y::Int16)
    
\n
  x+y\nend

Julia

Into:

function add(x::Int8,
 y::Int16)
    
\n
  x+y\nend\n@assert inferred(add,
 Tuple{Int8,
 Int16}
)
 == Int16

Julia

In order to do this we need to:

  1. Extract the type annotation ( Int8 , Int16 ) from the Abstract Syntax Tree ( AST )

  2. Write an inferred method that return the inferred return type

  3. Generate the final expression

For 1. we will use the MLStyle package which adds pattern matching for ASTs, making this task very easy.

For example if we want to extract the body of a comprehension [i^2 for i=1:10] using MLStyle's @match macro, we simply need to replace each element by wildcards [$body for $var=$range] :

using MLStyle\n
\nmatch_signature(ast)
 = @match ast begin\n
    quote\n
        $(::LineNumberNode)
\n
        
[$body for $var=$range]
\n
    end => body\nend\n
\nmatch_signature(
 quote 
[i^2 for i=1:10]
 end 
)

18.0s

MLStyle (Julia)

:(i ^ 2)

Note that Julia adds line numbers in its AST, so we also need to include a line number node in the pattern.

We can match our function signature in a similar fashion, returning the matched elements as a named tuple:

match_signature(ast)
 = @match ast begin\n
    quote\n
        $(::LineNumberNode)
\n
        function $name($(args...)
)
;
 $(block...)
 end\n
    end =>\n
    
(name = name,
\n
     args = args)
\nend\n
\nsig = match_signature(quote 
\n
    function add(x::Int8,y::Int16)
\n
        x+y\n
    end\nend)

1.7s

MLStyle (Julia)

(name = :add, args = Any[:(x::Int8), :(y::Int16)])

Similarly we define a method to extract type annotations ( x::T ):

match_type_annotation(ast)
 = @match ast begin :($(arg)::$(ann)
)
 => ann end\nmatch_type_annotation.(sig.args)

1.5s

MLStyle (Julia)

2-element Array{Symbol,1}:\n :Int8 \n :Int16

We can simply run type inference using Base.code_typed :

function inferred(f,args)
\n
    codeinfo = Base.code_typed(f,args)
[1]
\n
    codeinfo.second\nend\n
\n@assert inferred(
(x,y)
 -> x+y,
 Tuple{Int8,Int16}
)
 == Int16

0.6s

MLStyle (Julia)

When adding two integers of different type, the result gets automaticallypromoted(here to Int16 ). The promoted type can be computed using promote_type(T,K) .

We can now write a macro that will perform type checking:

macro checked(out,f)
\n
    sig  = match_signature(quote $f end)
\n
    args = match_type_annotation.(sig.args)
\n
  
\n
  \tchecks = quote\n
        @assert inferred($(sig.name)
,Tuple{$(args...)
}
)
 == $out\n
    end\n
    
\n
    esc(quote\n
        $f\n
        $checks\n
    end)
\nend\n
\n@macroexpand @checked Int16 function add(x::Int8,y::Int16)
\n
    x+y\nend

0.8s

MLStyle (Julia)

quote\n #= cell:10 =#\n function add(x::Int8, y::Int16)\n #= cell:16 =#\n x + y\n end\n #= cell:11 =#\n begin\n #= cell:6 =#\n if inferred(add, Tuple{Int8, Int16}) == Int16\n nothing\n else\n (Base.throw)((Base.AssertionError)("inferred(add, Tuple{Int8, Int16}) == Int16"))\n end\n end\nend

Here we use @macroexpand to see the code built by the macro. It first defines the function itself, and then adds a type check with the declared input and output types. We can now test that our macro work correctly:

@checked Int16 function add(x::Int8,y::Int16)
\n
    x+y\nend

0.4s

MLStyle (Julia)

If the inferred type is incorrect the macro will throw an AssertionError when the function is defined:

using Test\n@test_throws AssertionError @checked Int8 function add(x::Int8,y::Int16)
\n
    x+y\nend

1.0s

MLStyle (Julia)

Test Passed\n Thrown: AssertionError

Parametric methods

Next we will consider a simple parametric case, we will also replace the output type annotation by a predicate on the inferred type I to allow for more general (and useful) checks. For example in the following, the inferred type will be compared to the type of the first argument:

@checked I -> I == T function add(x::T,
 y::K)
 where 
{T<:Integer,
 K<:Integer}
 
\n
    x + y\nend

Julia

We first generalise our match_signature method to handle the parametric case, using an OR clause to handle functions without where :

match_signature(ast)
 = @match ast begin\n
    quote\n
        $(::LineNumberNode)
\n
        function $name($(args...)
)
 where 
{$(cov...)
}
;
 $(block...)
 end\n
    end ||\n
    quote\n
        $(::LineNumberNode)
\n
        function $name($(args...)
)
;
 $(block...)
 end\n
    end && Do(cov = 
[
]
)
 =>\n
    
(name = name,
\n
     args = args,
\n
     cov  = cov)
\nend\n
\nsig = match_signature(quote 
\n
    function add(x::T,y::K)
 where 
{T<:Integer,
 K<:Integer}
\n
        x+y\n
    end\nend)

2.1s

MLStyle (Julia)

(name = :add, args = Any[:(x::T), :(y::K)], cov = Any[:(T <: Integer), :(K <: Integer)])

We need a small method to extract subtype relations ( T <: Integer ):

match_subtype(ast)
 = @match ast begin :($(sub)<:$(sup)
)
 => 
(sub=sub,sup=sup)
 end\nmatch_subtype.(sig.cov)

1.5s

MLStyle (Julia)

2-element Array{NamedTuple{(:sub, :sup),Tuple{Symbol,Symbol}},1}:\n (sub = :T, sup = :Integer)\n (sub = :K, sup = :Integer)

In order to test every combination of parameters under the constrains above we need to enumerate all concret subtypes of Integer . This can be done by recursively calling InteractiveUtils.subtypes :

import InteractiveUtils.subtypes\n
\nfunction allsubtypes(T::Type)
\n
    isconcretetype(T)
 && return 
[T]
\n
    t = Type[
]
\n
    map(K->allsubtypes(K,t)
,
 subtypes(T)
)
\n
    t\nend\nallsubtypes(T,t)
 = isconcretetype(T)
 ? push!(t,T)
 : map(K->allsubtypes(K,t)
,
 subtypes(T)
)
\nallsubtypes(T::Symbol)
 = allsubtypes(Core.eval(Main,T)
)#convert Symbol to Type\n
\nallsubtypes(Integer)

2.4s

MLStyle (Julia)

12-element Array{Type,1}:\n Bool \n BigInt \n Int128 \n Int16 \n Int32 \n Int64 \n Int8 \n UInt128\n UInt16 \n UInt32 \n UInt64 \n UInt8

In order to enumerate every combination of concret types we can use Iterators.product :

collect(Iterators.product(
[Bool,BigInt]
,
[Bool,BigInt]
)
)

1.7s

MLStyle (Julia)

2×2 Array{Tuple{DataType,DataType},2}:\n (Bool, Bool) (Bool, BigInt) \n (BigInt, Bool) (BigInt, BigInt)

We also need a method that will take and abstract type annotation ( [ : T,:K] ) and replace it with the corresponding concrete subtypes:

function concretize!(args,
 concrete_args,
 idx)
\n
    for 
(j,K)
 in enumerate(concrete_args)
\n
        for i in idx[j]
\n
            args[i]
 = Symbol("$K")
\n
        end\n
    end\nend\n
\nargs = Symbol[:T,
 :K]
\nconcrete_args = 
(Bool,
 Bool)
\nidx = 
[
[1]
,
 
[2]
]
\n
\nconcretize!(args,
 concrete_args,
 idx)
\nargs

0.6s

MLStyle (Julia)

2-element Array{Symbol,1}:\n :Bool\n :Bool

Here idx contains the indices of where the abstract types annotations occurs in the method signature (they can occur at several positions, e.g. f(x::T, y::T, z::K) where {T,K} then idx = [[1,2],[3]] ).

We need to perform the same operation on our predicate (e.g. I -> I == T ), which can be an arbitrary expression. We will thus write a generic method that can replace a symbol by another in an expression:

#find all pair.first in expression and replace them by pair.second\nfunction ast_replace!(ex::Expr,
 pair::Pair)
\n
    for i=1:length(ex.args)
\n
        if ex.args[i]
 == pair.first\n
            ex.args[i]
 = pair.second\n
        else  
\n
            ast_replace!(ex.args[i]
,
 pair)
 
\n
        end\n
    end\n
    ex\nend 
\nast_replace!(ex::Symbol,
 pair)
 = nothing 
\nast_replace!(ex::LineNumberNode,
 pair)
 = nothing\n
\nast_replace!(:(I -> I == T)
,
 :T => Int8)

0.6s

MLStyle (Julia)

:(I->begin\n #= cell:15 =#\n I == Int8\n end)

We can now put all this together in a method that will generate the expression that performs the check for every combination of concrete parameters:

2.5s

MLStyle (Julia)

function check_types(fun,
 predicate,
 args,
 covs)
\n
  \t#setup\n
    out = Expr[
]
\n
    covs = match_subtype.(covs)
\n
    covs_symbols = 
[c.sub for c in covs]
\n
    idx  = 
[findall(args .== c)
 for c in covs_symbols]
\n
    
\n
    for p in Iterators.product(
[allsubtypes(c.sup)
 for c in covs]...)
 
\n
    
\n
    \t\t#build concrete arguments\n
        concrete_args = copy(args)
                
\n
\t\t\t  concretize!(concrete_args,
 p,
 idx)
\n
       
\n
        #build concrete predicate\n
        concrete_predicate = copy(predicate)
\n
        for pairs in 
[Pair(c,Symbol(t)
)
 for 
(c,t)
 in zip(covs_symbols,p)
]
\n
            ast_replace!(concrete_predicate,pairs)
\n
        end\n
        
\n
    \t\t#build final expression\n
        t = :(
 Tuple{$(concrete_args...)
}
 
)
\n
        I = :(
 inferred($fun,$t)
 
)
\n
        push!(out,
 quote 
\n
            report_type_check($concrete_predicate,$I,$fun,$concrete_args)
\n
        end)
\n
    end\n
    out\nend\n
\nfun,
 args = :add,
 
[:T,:K]
\ncovs = Any[:(T <: Integer)
,
 :(K <: Integer)
]
\nout_type = :(O -> O == T)
\n
\ncheck_types(fun,
 out_type,
 args,
 covs)
[1:2]

2.5s

MLStyle (Julia)

2-element Array{Expr,1}:\n quote\n #= cell:24 =#\n report_type_check((O->begin\n #= cell:32 =#\n O == Bool\n end), inferred(add, Tuple{Bool, Bool}), add, Symbol[:Bool, :Bool])\nend \n quote\n #= cell:24 =#\n report_type_check((O->begin\n #= cell:32 =#\n O == BigInt\n end), inferred(add, Tuple{BigInt, Bool}), add, Symbol[:BigInt, :Bool])\nend

In the generated code we call a helper function that will display the result of the type check instead of throwing an error:

function report_type_check(predicate,inferred,fun,types)
\n
    passed = predicate(inferred)
\n
    sig = join(types,",")
\n
    status = passed ? "SUCCESS" : "FAILURE"\n
    println(
 "$status: $(fun)($sig) \\t → $inferred" 
)
 
\nend

0.9s

MLStyle (Julia)

report_type_check (generic function with 1 method)

And finally we can update our checked macro:

macro checked(out,f)
\n
    sig = match_signature(quote $f end)
\n
    args = match_type_annotation.(sig.args)
\n
        
\n
    checks = check_types(Symbol(sig.name)
,out,args,sig.cov)
\n
    
\n
    esc(quote\n
        $f\n
        $(checks...)
\n
    end)
\nend\n
\n@checked O -> O == promote_type(T,K)
 function add(x::T,
 y::K)
 where 
{T<: Integer,
 K <: Integer}
 
\n
    x + y\nend

3.2s

MLStyle (Julia)

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n

All tests succeeded but the first one ( add(Bool,Bool) → Int64) since adding two booleans returns an Int64 . This could be fixed by modifying a bit the predicate to handle this particular case.

Limitations

One limitation of the implementation above is that it doesn't handle parametric types (e.g. Vector{T} where T <: Number ), but it should be relatively easy to generalize it to handle such a case. More general ones however might get tricky (e.g. Array{T,N} where {T <: Union{Number, String}, N} ).

The sheer number type combinations to check can also become an issue, for example there's 16 concrete subtype of Number in Base, so a function taking two numbers will generate 256 checks. There's 8998 concrete types in Base, so testing a function that has a single parameter annotated as Any would take that many checks, which is not very realistic.

For that reason this kind of tool might be better tailored for automatic tests, ran only during testing for some specific cases.

Final code

using MLStyle\nimport InteractiveUtils.subtypes\n
\nmatch_signature(ast)
 = @match ast begin\n
    quote\n
        $(::LineNumberNode)
\n
        function $name($(args...)
)
 where 
{$(cov...)
}
;
 $(block...)
 end\n
    end ||\n
    quote\n
        $(::LineNumberNode)
\n
        function $name($(args...)
)
;
 $(block...)
 end\n
    end && Do(cov = 
[
]
)
 =>\n
    
(name = name,
\n
     args = args,
\n
     cov  = cov)
\nend\n
\nmatch_type_annotation(ast)
 = @match ast begin :($(arg)::$(ann)
)
 => ann end\nmatch_subtype(ast)
 = @match ast begin :($(sub)<:$(sup)
)
 => 
(sub=sub,sup=sup)
 end\n
\nfunction inferred(f,args)
\n
    codeinfo = Base.code_typed(f,args)
[1]
\n
    codeinfo.second\nend\n
\nfunction allsubtypes(T::Type)
\n
    isconcretetype(T)
 && return 
[T]
\n
    t = Type[
]
\n
    map(K->allsubtypes(K,t)
,
 subtypes(T)
)
\n
    t\nend\nallsubtypes(T,t)
 = isconcretetype(T)
 ? push!(t,T)
 : map(K->allsubtypes(K,t)
,
 subtypes(T)
)
\nallsubtypes(T::Symbol)
 = allsubtypes(Core.eval(Main,T)
)#convert Symbol to Type\n
\nfunction concretize!(args,
 concrete_args,
 idx)
\n
    for 
(j,K)
 in enumerate(concrete_args)
\n
        for i in idx[j]
\n
            args[i]
 = Symbol("$K")
\n
        end\n
    end\nend\n
\nfunction ast_replace!(ex::Expr,
 pair::Pair)
\n
    for i=1:length(ex.args)
\n
        if ex.args[i]
 == pair.first\n
            ex.args[i]
 = pair.second\n
        else  
\n
            ast_replace!(ex.args[i]
,
 pair)
 
\n
        end\n
    end\n
    ex\nend 
\nast_replace!(ex::Symbol,
 pair)
 = nothing 
\nast_replace!(ex::LineNumberNode,
 pair)
 = nothing\n
\nfunction check_types(fun,
 predicate,
 args,
 covs)
\n
  \t#setup\n
    out = Expr[
]
\n
    covs = match_subtype.(covs)
\n
    covs_symbols = 
[c.sub for c in covs]
\n
    idx  = 
[findall(args .== c)
 for c in covs_symbols]
\n
    
\n
    for p in Iterators.product(
[allsubtypes(c.sup)
 for c in covs]...)
 
\n
    
\n
    \t\t#build concrete arguments\n
        concrete_args = copy(args)
                
\n
\t\t\t  concretize!(concrete_args,
 p,
 idx)
\n
       
\n
        #build concrete predicate\n
        concrete_predicate = copy(predicate)
\n
        for pairs in 
[Pair(c,Symbol(t)
)
 for 
(c,t)
 in zip(covs_symbols,p)
]
\n
            ast_replace!(concrete_predicate,pairs)
\n
        end\n
        
\n
    \t\t#build final expression\n
        t = :(
 Tuple{$(concrete_args...)
}
 
)
\n
        I = :(
 inferred($fun,$t)
 
)
\n
        push!(out,
 quote 
\n
            report_type_check($concrete_predicate,$I,$fun,$concrete_args)
\n
        end)
\n
    end\n
    out\nend\n
\nfunction report_type_check(predicate,inferred,fun,types)
\n
    passed = predicate(inferred)
\n
    sig = join(types,",")
\n
    status = passed ? "SUCCESS" : "FAILURE"\n
    println(
 "$status: $(fun)($sig) \\t → $inferred" 
)
 
\nend\n
\nmacro checked(out,f)
\n
    sig = match_signature(quote $f end)
\n
    args = match_type_annotation.(sig.args)
\n
        
\n
    checks = check_types(Symbol(sig.name)
,out,args,sig.cov)
\n
    
\n
    esc(quote\n
        $f\n
        $(checks...)
\n
    end)
\nend\n
\n@checked O -> O == promote_type(T,K)
 function add(x::T,
 y::K)
 where 
{T<: Integer,
 K <: Integer}
 
\n
    x + y\nend

Julia

"]]

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK