Extra general purpose functions, stubs, macros & meta types.
These functions, macros & types are either commonly used patterns, or mere stubs.
Function stubs are generically named functions without any actual body - they are, by default, noop. Every defined stub takes no arguments and do absolutely nothing.
These stubs are meant to be complementary to the Julia standard library. Similar to overloading Base.push!, you would
overload ExtraFun.use. Then, users of your library may simply using ExtraFun and call use(<your type>) without
having to worry about absolutely addressing the appropriate module. ExtraFun allows for shorter function names and thus
ease of use.
Following is an enumeration of all function stubs exported by ExtraFun, along with their respective intention. In turn, these intentions are merely intended to give you an idea what to use these stubs for.
Intended to cancel a time-consuming task, such as an intense computation or a blocking IO operation.
Intended to empty a collection or clear the state of an object.
Initialize something. Intended for deferred initialization of a resource. Possibly reopen an existing resource without having to fully reconstruct it, reusing previously supplied data.
Restore the state of an object from an external resource, typically a file or an internet resource. Forms the
complementary counterpiece to ExtraFun.store method.
Store the state of an object in an external resource, typically a file or an internet resource. Unlike the standard
library's Serialization.deserialize method, this method is intended for Julia-version and platform independent
serialization. For this purpose, it is advised to store a complementary file format version and/or parity data.
Intended to update the (internal) state of an object. Useful to defer comparatively heavy computations to the end of a cycle, for example.
Intended to indicate a change of state, either globally or locally to a container object.
Following are general purpose patterns packaged in functions (and possibly corresponding types) for convenience.
Simple functional negation of a callable. Useful to shorten down callbacks rather than using lambdas.
negate(callable)::BoolNaturally, it is assumed callable returns a boolean value.
isdiv3(x) = x % 3 == 0
filter!(negate(isdiv3), [1, 2, 3, 4])Simple negation of Base.isnothing(x).
truthy is a functional way of evaluating the "truth" of a value - as prominent in many other languages. In general, this means at least one bit is set. falsy is simply negate(truthy).
truthy(::Nothing) = false
truthy(b::Bool) = b
truthy(n::Number) = n != 0
truthy(_) = true
falsy(x) = !truthy(x)A functional alternative to Base.collect(coll). If coll is 1-dimensionally indexable (getindex(coll, i)), coll is returned directly. If coll is iterable (iterate(coll)), returns collect(coll). Otherwise, returns (coll,) (1-tuple containing only coll).
Return the passed-in argument if a signature of Base.iterate exists for it, otherwise return an iterable type around the argument. The result of this function will always be iterable.
Imperative general purpose functions.
Currying is a pattern where a new method is derived from an existing. When calling the curried method, positional arguments specified in the original curry call are prepended to the arguments of the curried call, and keyword arguments are added.
A macro to conveniently curry every single first-level function call also exists.
curry(callable, args...; kwargs...)function foo(num, factor; dofloor)
res = num * factor
if dofloor
res = floor(res)
end
return res
end
bar = curry(foo, 42; dofloor=true)
bar(0.5) # == 21
bar(2.1) # == 88Transform a camel-cased string into its underscored counterpiece. Useful e.g. to transform Symbols in macros.
decamelcase(str::AbstractString; uppercase::Bool = false)::AbstractStringdecamelcase("fooBarBaz") === "foo_bar_baz"
decamelcase("FoobarBaz") === "foobar_baz"
decamelcase("FooBarBaz", uppercase=true) === "FOO_BAR_BAZ"Finds the index of the given element in the array-like. If the element was not found, returns nothing.
indexof(ary, elem; by = identity, offset = 0, strict = false)::Integerby specifies a mapping callback on each element returning the mapped value to compare. The mapper is not called on elem.
offset specifies the 0-based offset from the start of the array-like to begin search.
strict specifies whether to use simple equality (==) or strict equality (===).
indexof([1, 2, 3], 5, by=(i)->i-2, strict=true) # == 3
indexof([1, 2, 3], 5) # == -1
indexof([1, 2, 3], 1, offset=2) # == -1Generated function pattern to test if a signature for Base.iterate(::T) exists.
Beware as this pattern may malbehave if such a signature is loaded after the first call to this generated function.
isiterable([]) # == true
isiterable(:foobar) # == false
isiterable(42) # == trueFunction pattern to test if a specific signature of a function exists.
hassignature(callable, argtypes::Type...)::Boolstruct MyStruct end
hassignature(push!, Vector{Int}) # == true
hassignature(push!, MyStruct) # == falseRetrieve and remove the first element from the array-like.
shift(ary::Iterable{T})::TNote that Iterable is not an actual type and used here merely for clarity.
The array-like must specialize Base.getindex and Base.deleteat! functions.
vec = [1, 2, 3]
shift(vec) # == 1
shift(vec) # == 2
vec # == [3]Insert an element at index 1 of an array-like.
unshift(ary::Iterable{T}, elem::T) -> aryNote that Iterable is not an actual type and is used here only for clarity.
The array-like must support the signature Base.insert!(::typeof(ary), 1, ::typeof(elem)).
unshift([2, 3, 4], 1) # == [1, 2, 3, 4]Insert a new element before or after an existing in a Vector.
Base.insert!(vec::Vector{T}, elem::T; [befure], [after], by = identity, strict::Bool = false)Either before or after keyword argument must be supplied, but not both. Otherwise, an ArgumentError is thrown.
by is a mapping callback transforming the elements of vec, but not before/after or elem. This is useful to, for example, insert elem before another which meets a specific condition.
strict can be used to specify whether to use strict equality (===) or simple equality (==).
struct Wrapper
int::Int
end
insert!(Wrapper.([1, 2, 3, 4, 6]), 5, before=6, by=(w)->w.int)
insert!(Wrapper.([1, 2, 3, 4, 6]), 5, after=4, by=(w)->w.int)Split a collection into two distinct ones where the first contains all elements for which a given condition returns true and the second all those for which it returns false.
Currently supports standard vectors and tuples.
split(condition, collection::Iterable{T})::Tuple{Vector{T}, Vector{T}}Note that Iterable is not an actual type and is used here only for clarity.
The first vector contains all items of collection for which condition returned true. The second vector contains all remaining items.
split(iseven, collect(1:10)) # == ([2, 4, 6, 8, 10], [1, 3, 5, 7, 9])ExtraFun provides a handful of useful yet simple macros. These include:
A convenience macro which curries every single first-level function call in its block expression. This is useful to call multiple functions reusing various identical arguments.
@curry 0xFF42 file = stderr begin
println("foobar") # prints "0xFF42 foobar" to stderr
println(42) # prints "0xFF42 42" to stderr
endA convenience macro which ensures the given code is only executed once per session.
function foo(n)
@once n > 512 println("parameter exceeds safety threshold")
n+1
end
foo(513)
# prints: parameter exceeds safety threshold
foo(514)
# does not printA simple string prefix to produce a symbol. Literally equivalent to Symbol(str). The advantage of using the sym""
notation is that it allows using characters otherwise illegal in : notation whilst shortening syntax slightly.
Resource management inspired by other languages' with keyword. It generates Julia code in the following syntax:
@with resources... block
# is (almost) equivalent to
try
let resources...
block
end
finally
close.(resources)
endUsage is similar to other languages' with keyword:
res1 = SomeCloseableResource()
@with res1 res2 = SomeCloseableResource() SomeCloseableResource() begin
println(res1)
println(res2)
end
println(res1)
# res2 and last resource undefined hereNote: For res1 above to work, SomeCloseableResource() should be or contain a reference to the closeable resource. If
it can be copied bitwise, res1 may remain unchanged outside of @with.
General purpose and simple types.
A simple mutable wrapper around a single field of type T. The Mutable type comes in handy either as a way to reference variables, or to mark a single field of an otherwise immutable struct as mutable.
struct Mutable{T}
value::T
endusing ExtraFun
struct Immutable
immutable::Int
mutable::Mutable{Bool}
end
Immutable(immutable, mutable::Bool) = Immutable(immutable, Mutable(mutable))
myvar = Immutable(42, false)
myvar.mutable[] # == false
myvar.mutable[] = true
myvar.mutable[] # == true
myvar.immutable += 1 # throwsWrapper around a Task object with a specialization of ExtraFun.cancel to cancel cancel a blocking and/or yielding task prematurely. Unfortunately, these cannot be used with @sync and @async.
To conveniently create such a task, the with_cancel method is introduced. Its signature is as follows:
with_cancel(callback, schedule_immediately::Bool = false)::CancellableTaskusing ExtraFun
task1 = with_cancel() do
sleep(9999)
end
task2 = with_cancel() do
return 42
end
task3 = with_cancel() do
throw("foobar")
end
cancel(task1)
wait(task1) # throws TaskFailedException wrapping CancellationError
fetch(task2) == 42 # success
wait(task3) # throws TaskFailedException wrapping "foobar"Wrapper around a Task object with an automatic timeout. The timeout only affects the task if it blocks and/or yields. One can Base.wait, Base.fetch, or ExtraFun.cancel the task. Like a CancellableTask, the CancellationError thrown by Base.wait and Base.fetch will be wrapped by a TaskFailedException. Analogously, the TimeoutError triggered upon timing out will also be wrapped in such a TaskFailedException. Like CancellableTask, these tasks are incompatible with @sync and @async.
To conveniently create such a task, the with_timeout method is introduced. Its signature is as follows:
with_timeout(callback, timeout::Real; schedule_immediately::Bool)::TimeoutTaskusing ExtraFun
task1 = with_timeout(2) do
sleep(3)
end
task2 = with_timeout(2) do
return 42
end
task3 = with_timeout(2) do
sleep(3)
end
task4 = with_timeout(3) do
throw("foobar")
end
wait(task1) # throws TaskFailedException wrapping TimeoutError
fetch(task2) == 42 # success
cancel(task3)
wait(task3) # throws TaskFailedException wrapping CancellationError
wait(task4) # throws TaskFailedException wrapping "foobar"Meta Types are types (abstract or concrete) which either provide additional information on other types, or merely convey additional information to the compiler. In the simplest instance, this allows adjusting the behavior of otherwise identical functions, or, vice versa, customizing the behavior of an otherwise identical structure.
The Ident meta type does not contain any information. It is designed to enable the compiler to dispatch based on
actual Symbol values as opposed to the Symbol type.
struct Ident{S} endstruct Ident{S} end
extract(::Ident{:foo}) = 42
extract(::Ident{:bar}) = 69.69ExtraFun introduces an Optional{S, T} meta type which represents a value which theoretically exists but may or may not be loaded at the time. If the value is not loaded, the Optional will contain unknown - the only instance of Unknown, similar to nothing and Nothing. While T can be any type, S is a vanity parameter intended as a unique identifier for your Optional, allowing specialization of Base.getindex(::Optional{S}) while retaining interoperability with other Optionals of other S.
The signature of Optional is rather complex. Plentiful specializations of Base.convert allow you to use it in the most intuitive ways. Generally, the S identifier can be ignored; it will default to :generic. It is only relevant to retrieving the actual value of the Optional in case the current value is unknown.
Getting and setting the value proceeds much like Ref through Base.getindex and Base.setindex!, or rather opt[] and opt[] = value. The default implementation of Base.getindex tests if the wrapped value is unknown. If so, it calls ExtraFun.load(::Optional), caches its return value, and passes it on. The default implementation of Base.setindex! always overrides the value regardless. All of the above methods may be specialized on your S identifier.
Alternatively, you may test if an Optional contains unknown with ExtraFun.isunknown, and then ExtraFun.load it with additional arguments if necessary.
Generally, whichever usage you imagine is probably possible. If not, drop me an issue and I'll see about implementing it!
Some examples:
Optional() # == Optional{:generic, Any}(unknown)
Optional(42) # == Optional{:generic, Int}(42)
Optional(:myoptional, 24)
Optional{:myoptional}() # == Optional{:myoptional, Any}(unknown)
Optional{:foo, Real}() # == Optional{:foo, Real}(unknown)
Optional{:bar, Integer}(42.) # == Optional{:bar, Integer}(42)
struct Foo
opt::Optional{:foo, Float32}
end
Foo() = Foo(unknown)
Foo() # == Foo(Optional{:foo, Float32}(unknown))
Foo(42) # == Foo(Optional{:foo, Float32}(42.0f0))
Foo(42).opt[] === 42.f0
Foo(Optional(42)) # == Foo(Optional{:foo, Float32}(42.0f0))
Foo().opt = 42 # |-- These are equivalent due to Base.convert
Foo().opt[] = 42 # |--
struct Bar
opt::Optional{:bar}
end
Bar(42) # == Bar(Optional{:bar, Int}(42))
Bar(Optional{:generic, Integer}(42)) # == Bar(Optional{:generic, Integer}(42))
struct Baz
opt::Optional{S, Float32} where S
end
ExtraFun.load(opt::Optional{:baz}) = opt.value = 69.69
ExtraFun.load(io::IO, opt::Optional{:baz}) = opt.value = read(io, Float32)
Baz(42) # == Baz(Optional{:generic, Float32}(42.0f0))
Baz(Optional{:baz, Real}(42)) # == Baz(Optional{:baz, Float32}(42.0f0))
Baz(Optional(:baz, 42)) # == Baz(Optional{:baz, Float32}(42.0f0))
Baz(unknown).opt[] ≈ 69.69
let baz = Baz(unknown)
buff = IOBuffer() # Prepare external storage
buff.write(24.f0)
buff.seek(0)
load(buff, baz)
baz.opt[] == 24.f0
endSometimes, simply calling load(optional) is not enough. You may depend on additional arguments such as a file handle. In that case, manually
A more complex pattern which ExtraFun provides is the xcopy function and macro family. These allow customizing by various degrees of depth how an object is copied.
Copies the template object, overriding the copy's fields by keyword arguments.
xcopy(x::T)::Tstruct MyStruct
int::Int
flag::Bool
end
@xcopy MyStruct
xcopy(MyStruct(0, false), int=42) # MyStruct(42, false)Makes a given type xcopyable; xcopy is by design not generic.
@xcopy(T::Type)Actually constructs a new instance of the same type of the source object.
xcopy_construct(tpl::T, args...; kwargs...)::TCreates a new instance of T with specified args and kwargs. Specializations may change the behavior entirely, or simply add further initialization based on tpl. The arguments - both positional and keyword - are received from xcopy which copies these either from tpl or uses a customized/overridden value.
Normally, it won't be necessary to override this method, but it can be useful to trigger additional logic upon the newly constructed object.
Retrieves the copied value for the copied object. By default, retrieves tpl's own field. If the field itself is Base.copyable, it is copied. Otherwise, it is returned directly (referenced).
xcopy_override(tpl, ::FieldCopyOverride{F})::AnyF is a Symbol representing the field name for which to retrieve the copied value.
Specializations may specialize this method to further customize the behavior of copying individual fields of tpl. However, it is strongly advised to use @xcopy_override to implement such a specialization for convenience.
Convenience macro to specialize xcopy_override.
@xcopy_override(T::Type, S::Symbol, expr::Expr)T is the type for which the xcopy is being implemented. S is the field for which the copied value is overridden. expr is the expression used to compute the overridden value.
struct MyStruct
int::Int
flag::Bool
end
@xcopy MyStruct
@xcopy_override MyStruct :int tpl.int + 1
xcopy(MyStruct(1, false)) == MyStruct(2, false) # == true