Skip to content

Instantly share code, notes, and snippets.

@catawbasam
Last active August 29, 2015 14:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save catawbasam/99043bb74519378428fd to your computer and use it in GitHub Desktop.
Save catawbasam/99043bb74519378428fd to your computer and use it in GitHub Desktop.
Julia SubString: isvalid ind2chr chr2ind
{
"metadata": {
"language": "Julia",
"name": "",
"signature": "sha256:64712e134a63248c7287bc1cf7858ccd7adc08a2821cce960ef95b28f51ce2e2"
},
"nbformat": 3,
"nbformat_minor": 0,
"worksheets": [
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## More SubString{DirectIndexString} specialization for performance\n",
"\n",
"```\n",
"IN length(s::DirectIndexString) = endof(s) \n",
"Yes isvalid(s::DirectIndexString, i::Integer)\n",
"Yes ind2chr(s::DirectIndexString, i::Integer) \n",
"Yes chr2ind(s::DirectIndexString, i::Integer) \n",
"? prevind()\n",
"? nextind()\n",
"```\n",
"\n",
"TODO: check print/show etc for SubString"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"using Base.Test\n",
"using Benchmark"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 3
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"#compare speed of 2 zero-argument functions\n",
"function abspeed(a::Function, b::Function, n::Integer)\n",
" val=a()\n",
" val=b() #warm-up\n",
" t0= Base.time_ns()\n",
" for i in 1:n\n",
" val=a()\n",
" end \n",
" \u03bcs_a=(Base.time_ns()-t0)/1000.0 #microseconds\n",
"\n",
" t0= Base.time_ns()\n",
" for i in 1:n\n",
" val=b()\n",
" end \n",
" \u03bcs_b=(Base.time_ns()-t0)/1000.0 #microseconds\n",
"\n",
" return (\u03bcs_a, \u03bcs_b, \u03bcs_b/\u03bcs_a) \n",
"end "
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 4,
"text": [
"abspeed (generic function with 1 method)"
]
}
],
"prompt_number": 4
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# check whether the speed ratio is close to 1.0 !!! If not, worry.\n",
"function fx()\n",
" return string(\"blah\")\n",
"end\n",
"for i in 1:4\n",
" println( abspeed(fx,fx, 2000))\n",
"end"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"(33"
]
},
{
"output_type": "stream",
"stream": "stdout",
"text": [
".353,33.272,0.9975714328546157)\n",
"(36.431,33.287,0.913699870988993)\n",
"(33.297,33.279,0.9994594107577262)\n",
"(33.293,37.905,1.1385276184182862)\n"
]
}
],
"prompt_number": 5
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"address=ascii(\"\"\"Four score and seven years ago \n",
"our fathers brought forth on this continent a new nation, \n",
"conceived in liberty, and dedicated to the proposition \n",
"that all men are created equal.\n",
"Now we are engaged in a great civil war, testing whether that nation, \n",
"or any nation so conceived and so dedicated, can long endure. \n",
"\n",
"We are met on a great battlefield of that war. \n",
"We have come to dedicate a portion of that field, \n",
"as a final resting place for those \n",
"who here gave their lives that that nation might live. \n",
"It is altogether fitting and proper that we should do this.\n",
"But, in a larger sense, we can not dedicate, \n",
"we can not consecrate, we can not hallow this ground. \n",
"The brave men, living and dead, who struggled here, have consecrated it, \n",
"far above our poor power to add or detract. The world will little note, \n",
"nor long remember what we say here, but it can never forget what they did here. \n",
"It is for us the living, rather, to be dedicated here to the unfinished work \n",
"which they who fought here have thus far so nobly advanced. \n",
"\n",
"It is rather for us to be here dedicated to the great task \n",
"remaining before us that from these honored dead \n",
"we take increased devotion to that cause for which they gave \n",
"the last full measure of devotion \n",
"that we here highly resolve that these dead shall not have died in vain\n",
"that this nation, under God, shall have a new birth of freedom\n",
"and that government of the people, by the people, for the people, \n",
"shall not perish from the earth.\n",
"\"\"\");\n",
"\n"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 6
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"ss=SubString(address, 118, 177)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 7,
"text": [
"\"dedicated to the proposition \\nthat all men are created equal\""
]
}
],
"prompt_number": 7
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# for comparison of ASCIIString to SubString{ASCIIString}\n",
"sstr=\"dedicated to the proposition \\nthat all men are created equal\""
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 8,
"text": [
"\"dedicated to the proposition \\nthat all men are created equal\""
]
}
],
"prompt_number": 8
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# for unit tests\n",
"s=\"lorem ipsum\"\n",
"sdict=[SubString(s,1,11)=>s, \n",
" SubString(s,1,6)=>\"lorem \",\n",
" SubString(s,1,0)=>\"\", \n",
" SubString(s,2,4)=>\"ore\", \n",
" SubString(s,2,16)=>\"orem ipsum\", \n",
" SubString(s,12,14)=>\"\" \n",
" ] "
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 9,
"text": [
"Dict{SubString{ASCIIString},ASCIIString} with 5 entries:\n",
" \"lorem \" => \"lorem \"\n",
" \"orem ipsum\" => \"orem ipsum\"\n",
" \"lorem ipsum\" => \"lorem ipsum\"\n",
" \"\" => \"\"\n",
" \"ore\" => \"ore\""
]
}
],
"prompt_number": 9
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# isvalid()\n",
"Yes, isvalid(SubString{DirectIndexString}, i::Integer) specialization is 30% faster on a simple micro-benchmark. code_typed also looks better."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"@which isvalid(ss,2) # not specialized"
],
"language": "python",
"metadata": {},
"outputs": [
{
"html": [
"isvalid(s::<b>String</b>,i::<b>Integer</b>) at <a href=\"https://github.com/JuliaLang/julia/tree/1e046c53af26b518ad831708b2962e625381062f/base/string.jl#L98\" target=\"_blank\">string.jl:98</a>"
],
"metadata": {},
"output_type": "pyout",
"prompt_number": 10,
"text": [
"isvalid(s::String,i::Integer) at string.jl:98"
]
}
],
"prompt_number": 10
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"#Proposed specialization\n",
"isvalidX{T<:DirectIndexString}(s::SubString{T}, i::Integer) = (start(s) <= i <= endof(s))"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 11,
"text": [
"isvalidX (generic function with 1 method)"
]
}
],
"prompt_number": 11
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### isvalid() Unit tests"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# null output is good!\n",
"for (sst,st) in sdict\n",
" for i in -1:12\n",
" #println(i)\n",
" @test isvalid(st,i)==isvalidX(sst,i)\n",
" end\n",
"end"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 12
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### isvalid() Speed check on ASCIIString-- \n",
"on this test, the proposed version runs in about 60% the time of the current version."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"test_isvalid() = isvalid(ss,34)\n",
"test_isvalidX() = isvalidX(ss,34)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 13,
"text": [
"test_isvalidX (generic function with 1 method)"
]
}
],
"prompt_number": 13
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"for i in 1:4\n",
" println( abspeed(test_isvalid, test_isvalidX, 1000))\n",
"end"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"("
]
},
{
"output_type": "stream",
"stream": "stdout",
"text": [
"48.888,30.709,0.6281500572737686)\n",
"(47.662,31.475,0.6603793378372709)\n",
"(49.871,31.739,0.6364219686791923)\n",
"(49.954,31.803,0.6366457140569324)\n"
]
}
],
"prompt_number": 14
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### isvalid() code_typed() comparison"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"code_typed(isvalid,(SubString{DirectIndexString},Int))"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 15,
"text": [
"1-element Array{Any,1}:\n",
" :($(Expr(:lambda, {:s,:i}, {{:#s465},{{:s,SubString{DirectIndexString},0},{:i,Int64,0},{:#s465,Bool,18}},{}}, :(begin # string.jl, line 98:\n",
" $(Expr(:enter, 0)) # line 99:\n",
" next(s::SubString{DirectIndexString},i::Int64)::(Char,Int64) # line 100:\n",
" #s465 = true\n",
" $(Expr(:leave, 1))\n",
" return #s465::Bool\n",
" 0: \n",
" $(Expr(:leave, 1))\n",
" return false\n",
" end::Bool))))"
]
}
],
"prompt_number": 15
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"code_typed(isvalidX,(SubString{ASCIIString},Int64))"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 16,
"text": [
"1-element Array{Any,1}:\n",
" :($(Expr(:lambda, {:s,:i}, {{},{{:s,SubString{ASCIIString},0},{:i,Int64,0}},{}}, :(begin # In[11], line 2:\n",
" unless top(sle_int)(1,i::Int64)::Bool goto 0\n",
" return top(sle_int)(i::Int64,top(getfield)(s::SubString{ASCIIString},:endof)::Int64)::Bool\n",
" 0: \n",
" return false\n",
" end::Bool))))"
]
}
],
"prompt_number": 16
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ind2chr and chr2ind"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"@which ind2chr(ss,2)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"html": [
"ind2chr(s::<b>String</b>,i::<b>Integer</b>) at <a href=\"https://github.com/JuliaLang/julia/tree/1e046c53af26b518ad831708b2962e625381062f/base/string.jl#L146\" target=\"_blank\">string.jl:146</a>"
],
"metadata": {},
"output_type": "pyout",
"prompt_number": 17,
"text": [
"ind2chr(s::String,i::Integer) at string.jl:146"
]
}
],
"prompt_number": 17
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"@which chr2ind(ss,2)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"html": [
"chr2ind(s::<b>String</b>,i::<b>Integer</b>) at <a href=\"https://github.com/JuliaLang/julia/tree/1e046c53af26b518ad831708b2962e625381062f/base/string.jl#L160\" target=\"_blank\">string.jl:160</a>"
],
"metadata": {},
"output_type": "pyout",
"prompt_number": 18,
"text": [
"chr2ind(s::String,i::Integer) at string.jl:160"
]
}
],
"prompt_number": 18
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"from string.jl:\n",
"```\n",
"ind2chr(s::DirectIndexString, i::Integer) = i\n",
"chr2ind(s::DirectIndexString, i::Integer) = i\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### proposed methods:"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"#proposed specialization\n",
"#ind2chrX{T<:DirectIndexString}(s::SubString{T}, i::Integer) = start(s)<= i <= endof(s) ? i : throw(BoundsError)\n",
"#chr2indX{T<:DirectIndexString}(s::SubString{T}, i::Integer) = start(s)<= i <= endof(s) ? i : throw(BoundsError)\n",
"Base.checkbounds(s::String, i::Integer) = start(s) <= i <= endof(s) || throw(BoundsError())\n",
"ind2chrX{T<:DirectIndexString}(s::SubString{T}, i::Integer) = begin checkbounds(s,i); i end\n",
"chr2indX{T<:DirectIndexString}(s::SubString{T}, i::Integer) = begin checkbounds(s,i); i end"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 49,
"text": [
"chr2indX (generic function with 1 method)"
]
}
],
"prompt_number": 49
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"i=0\n",
"start(ss)<= i <= endof(ss) ? i : throw(BoundsError)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"ename": "LoadError",
"evalue": "BoundsError\nwhile loading In[50], in expression starting on line 2",
"output_type": "pyerr",
"traceback": [
"BoundsError\nwhile loading In[50], in expression starting on line 2"
]
}
],
"prompt_number": 50
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"ind2chr(sstr, 5) #from ASCIIString"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 51,
"text": [
"5"
]
}
],
"prompt_number": 51
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"ind2chr(ss, 5) #current SubString"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 52,
"text": [
"5"
]
}
],
"prompt_number": 52
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"ind2chrX(ss, 5) #proposed SubString{DirectIndexString}"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 53,
"text": [
"5"
]
}
],
"prompt_number": 53
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### ind2chr() and 2chr2ind() unit tests"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"s1=\"hello\"\n",
"ss1=SubString(s1,1,5)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 54,
"text": [
"\"hello\""
]
}
],
"prompt_number": 54
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"ind2chr(ss1,11) "
],
"language": "python",
"metadata": {},
"outputs": [
{
"ename": "LoadError",
"evalue": "BoundsError()\nwhile loading In[55], in expression starting on line 1",
"output_type": "pyerr",
"traceback": [
"BoundsError()\nwhile loading In[55], in expression starting on line 1",
" in getindex at ./string.jl:613",
" in ind2chr at string.jl:146"
]
}
],
"prompt_number": 55
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"ind2chrX(ss1,11) "
],
"language": "python",
"metadata": {},
"outputs": [
{
"ename": "LoadError",
"evalue": "BoundsError()\nwhile loading In[56], in expression starting on line 1",
"output_type": "pyerr",
"traceback": [
"BoundsError()\nwhile loading In[56], in expression starting on line 1",
" in checkbounds at In[49]:4"
]
}
],
"prompt_number": 56
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# compare current to proposed\n",
"# null output is good!\n",
"function unittest_ind2chr()\n",
" for (ss,s) in sdict\n",
" for i in 1:length(ss)\n",
" #println(\"$i $(dump(ss))\")\n",
" @test ind2chr(ss,i)==ind2chrX(ss,i)\n",
" end\n",
" end\n",
"end\n",
"unittest_ind2chr()"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 57
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# null output is good!\n",
"function unittest_chr2ind()\n",
" for (ss,s) in sdict\n",
" for i in 1:length(ss)\n",
" #println(\"$i $(dump(ss))\")\n",
" @test chr2ind(ss,i)==chr2indX(ss,i)\n",
" end\n",
" end\n",
"end\n",
"unittest_chr2ind()"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 58
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### ind2chr() and chr2ind() speed check on ASCIIString \n",
"on this test, \n",
"* proposed ind2chr() takes ~22% the time of the current version \n",
"* proposed chr2ind() takes ~25% the time of the current version \n",
"i.e. they run about 4-5x faster."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"test_ind2chr() = ind2chr(ss,34)\n",
"test_ind2chrX() = ind2chrX(ss,34)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 64,
"text": [
"test_ind2chrX (generic function with 1 method)"
]
}
],
"prompt_number": 64
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"for i in 1:4\n",
" println( abspeed(test_ind2chr, test_ind2chrX, 1000))\n",
"end"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"(145"
]
},
{
"output_type": "stream",
"stream": "stdout",
"text": [
".163,33.651,0.23181526973126762)\n",
"(149.828,35.64,0.23787276076567798)\n",
"(144.439,33.564,0.23237491259285928)\n",
"(154.224,33.575,0.21770282186948858)\n"
]
}
],
"prompt_number": 65
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"1/.17"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 66,
"text": [
"5.88235294117647"
]
}
],
"prompt_number": 66
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"test_chr2ind() = chr2ind(ss,34)\n",
"test_chr2indX() = chr2indX(ss,34)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 67,
"text": [
"test_chr2indX (generic function with 1 method)"
]
}
],
"prompt_number": 67
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"for i in 1:4\n",
" println( abspeed(test_chr2ind, test_chr2indX, 1000))\n",
"end"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"(133"
]
},
{
"output_type": "stream",
"stream": "stdout",
"text": [
".628,35.531,0.265894872332146)\n",
"(137.627,36.584,0.2658199335886127)\n",
"(132.505,35.443,0.26748424587751407)\n",
"(132.956,35.429,0.26647161466951474)\n"
]
}
],
"prompt_number": 68
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"1/.14"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 69,
"text": [
"7.142857142857142"
]
}
],
"prompt_number": 69
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### ind2chr() code_typed"
]
},
{
"cell_type": "code",
"collapsed": true,
"input": [
"code_typed(ind2chr,(SubString{DirectIndexString},Int))"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 70,
"text": [
"1-element Array{Any,1}:\n",
" :($(Expr(:lambda, {:s,:i}, {{:j,:k,:#s467,:#s465,:c,:#s464,:l,:#s466,:_var0,:_var1,:_var2,:_var3},{{:s,SubString{DirectIndexString},0},{:i,Int64,0},{:j,Int64,2},{:k,Int64,2},{:#s467,(Char,Int64),18},{:#s465,(Char,Int64),18},{:c,Char,18},{:#s464,(Int64,Int64),18},{:l,Int64,18},{:#s466,Int64,2},{:_var0,Char,18},{:_var1,Int64,18},{:_var2,Int64,18},{:_var3,Int64,18}},{}}, :(begin # string.jl, line 146:\n",
" getindex(s::SubString{DirectIndexString},i::Int64)::Char # line 147:\n",
" j = 1 # line 148:\n",
" k = 1 # line 149:\n",
" unless true goto 1\n",
" 2: # line 150:\n",
" #s467 = next(s::SubString{DirectIndexString},k::Int64)::(Char,Int64)\n",
" #s466 = 1\n",
" _var0 = tupleref(#s467::(Char,Int64),1)::Union(Char,Int64)\n",
" _var1 = box(Int64,add_int(1,1))::Int64\n",
" c = _var0::Char\n",
" #s466 = _var1::Int64\n",
" _var2 = tupleref(#s467::(Char,Int64),2)::Union(Char,Int64)\n",
" _var3 = box(Int64,add_int(2,1))::Int64\n",
" l = _var2::Int64\n",
" #s466 = _var3::Int64 # line 151:\n",
" unless sle_int(i::Int64,k::Int64)::Bool goto 4 # line 152:\n",
" return j::Int64\n",
" 4: # line 154:\n",
" j = box(Int64,add_int(j::Int64,1))::Int64 # line 155:\n",
" k = l::Int64\n",
" 3: \n",
" unless false goto 2\n",
" 1: \n",
" 0: \n",
" return\n",
" end::Int64))))"
]
}
],
"prompt_number": 70
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"code_typed(ind2chrX,(SubString{DirectIndexString},Int))"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 71,
"text": [
"1-element Array{Any,1}:\n",
" :($(Expr(:lambda, {:s,:i}, {{},{{:s,SubString{DirectIndexString},0},{:i,Int64,0}},{}}, :(begin # In[49], line 5: # In[49], line 5:\n",
" checkbounds(s::SubString{DirectIndexString},i::Int64)::Bool # line 5:\n",
" return i::Int64\n",
" end::Int64))))"
]
}
],
"prompt_number": 71
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"#;ipython nbconvert \"SubString_DirectIndexString.ipynb\""
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 72
}
],
"metadata": {}
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment