Skip to content

Commit

Permalink
Update manual for Val(x) instance usage
Browse files Browse the repository at this point in the history
  • Loading branch information
Andy Ferris committed Jul 5, 2017
1 parent f0a91f7 commit 5ad5d93
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 24 deletions.
27 changes: 14 additions & 13 deletions doc/src/manual/performance-tips.md
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ loaded from an input file that might contain either integers, floats, strings, o

## Types with values-as-parameters

Let's say you want to create an `N`-dimensional array that has size 3 along each axis. Such arrays
Let's say you want to create an `N`-dimensional array that has size 3 along each axis. Such arrays
can be created like this:

```jldoctest
Expand Down Expand Up @@ -685,37 +685,37 @@ slow.

Now, one very good way to solve such problems is by using the [function-barrier technique](@ref kernal-functions).
However, in some cases you might want to eliminate the type-instability altogether. In such cases,
one approach is to pass the dimensionality as a parameter, for example through `Val{T}` (see
one approach is to pass the dimensionality as a parameter, for example through `Val{T}()` (see
["Value types"](@ref)):

```jldoctest
julia> function array3(fillval, ::Type{Val{N}}) where N
fill(fillval, ntuple(d->3, Val{N}))
julia> function array3(fillval, ::Val{N}) where N
fill(fillval, ntuple(d->3, Val(N)))
end
array3 (generic function with 1 method)
julia> array3(5.0, Val{2})
julia> array3(5.0, Val(2))
3×3 Array{Float64,2}:
5.0 5.0 5.0
5.0 5.0 5.0
5.0 5.0 5.0
```

Julia has a specialized version of `ntuple` that accepts a `Val{::Int}` as the second parameter;
by passing `N` as a type-parameter, you make its "value" known to the compiler. Consequently,
this version of `array3` allows the compiler to predict the return type.
Julia has a specialized version of `ntuple` that accepts a `Val{::Int}` instance as the second
parameter; by passing `N` as a type-parameter, you make its "value" known to the compiler.
Consequently, this version of `array3` allows the compiler to predict the return type.

However, making use of such techniques can be surprisingly subtle. For example, it would be of
no help if you called `array3` from a function like this:

```julia
function call_array3(fillval, n)
A = array3(fillval, Val{n})
A = array3(fillval, Val(n))
end
```

Here, you've created the same problem all over again: the compiler can't guess the type of `n`,
so it doesn't know the type of `Val{n}`. Attempting to use `Val`, but doing so incorrectly, can
Here, you've created the same problem all over again: the compiler can't guess what `n` is,
so it doesn't know the *type* of `Val(n)`. Attempting to use `Val`, but doing so incorrectly, can
easily make performance *worse* in many situations. (Only in situations where you're effectively
combining `Val` with the function-barrier trick, to make the kernel function more efficient, should
code like the above be used.)
Expand All @@ -724,13 +724,14 @@ An example of correct usage of `Val` would be:

```julia
function filter3(A::AbstractArray{T,N}) where {T,N}
kernel = array3(1, Val{N})
kernel = array3(1, Val(N))
filter(A, kernel)
end
```

In this example, `N` is passed as a parameter, so its "value" is known to the compiler. Essentially,
`Val{T}` works only when `T` is either hard-coded (`Val{3}`) or already specified in the type-domain.
`Val(T)` works only when `T` is either hard-coded/literal (`Val(3)`) or already specified in the
type-domain.

## The dangers of abusing multiple dispatch (aka, more on types with values-as-parameters)

Expand Down
23 changes: 12 additions & 11 deletions doc/src/manual/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -1260,37 +1260,38 @@ floating-point numbers, tuples, etc.) as type parameters. A common example is t
parameter in `Array{T,N}`, where `T` is a type (e.g., [`Float64`](@ref)) but `N` is just an `Int`.

You can create your own custom types that take values as parameters, and use them to control dispatch
of custom types. By way of illustration of this idea, let's introduce a parametric type, `Val{T}`,
which serves as a customary way to exploit this technique for cases where you don't need a more
elaborate hierarchy.
of custom types. By way of illustration of this idea, let's introduce a parametric type, `Val{x}`,
and a constructor `Val(x) = Val{x}()`, which serves as a customary way to exploit this technique
for cases where you don't need a more elaborate hierarchy.

`Val` is defined as:

```jldoctest valtype
julia> struct Val{T}
julia> struct Val{x}
end
Base.@pure Val(x) = Val{x}()
```

There is no more to the implementation of `Val` than this. Some functions in Julia's standard
library accept `Val` types as arguments, and you can also use it to write your own functions.
library accept `Val` instances as arguments, and you can also use it to write your own functions.
For example:

```jldoctest valtype
julia> firstlast(::Type{Val{true}}) = "First"
julia> firstlast(::Val{true}) = "First"
firstlast (generic function with 1 method)
julia> firstlast(::Type{Val{false}}) = "Last"
julia> firstlast(::Val{false}) = "Last"
firstlast (generic function with 2 methods)
julia> firstlast(Val{true})
julia> firstlast(Val(true))
"First"
julia> firstlast(Val{false})
julia> firstlast(Val(false))
"Last"
```

For consistency across Julia, the call site should always pass a `Val`*type* rather than creating
an *instance*, i.e., use `foo(Val{:bar})` rather than `foo(Val{:bar}())`.
For consistency across Julia, the call site should always pass a `Val`*instance* rather than using
a *type*, i.e., use `foo(Val(:bar))` rather than `foo(Val{:bar})`.

It's worth noting that it's extremely easy to mis-use parametric "value" types, including `Val`;
in unfavorable cases, you can easily end up making the performance of your code much *worse*.
Expand Down

0 comments on commit 5ad5d93

Please sign in to comment.