Repository: invenia/NamedDims.jl
Pull request #9 (2019-04-17 19:48:31)

Add Broadcasting
This is the PR for broadcasting.
I am currently pointing it it #7 
just so the differences can be seen easily.
And so can parallelize reviewing

After #7 is merged I will rebase this and re-open this PR against master

codecov[bot]: # [Codecov](https://codecov.io/gh/invenia/NamedDims.jl/pull/9?src=pr&el=h1) Report
> Merging [#9](https://codecov.io/gh/invenia/NamedDims.jl/pull/9?src=pr&el=desc) into [ox/basic_math](https://codecov.io/gh/invenia/NamedDims.jl/commit/e7d0fbbeb91d94b23b286597e850142e2cf960ee?src=pr&el=desc) will **decrease** coverage by `1.6%`.
> The diff coverage is `83.33%`.

[![Impacted file tree graph](https://codecov.io/gh/invenia/NamedDims.jl/pull/9/graphs/tree.svg?width=650&token=M5MW3xa0xM&height=150&src=pr)](https://codecov.io/gh/invenia/NamedDims.jl/pull/9?src=pr&el=tree)

```diff
@@               Coverage Diff                @@
##           ox/basic_math      #9      +/-   ##
================================================
- Coverage          93.91%   92.3%   -1.61%     
================================================
  Files                  6       7       +1     
  Lines                115     130      +15     
================================================
+ Hits                 108     120      +12     
- Misses                 7      10       +3
```


| [Impacted Files](https://codecov.io/gh/invenia/NamedDims.jl/pull/9?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/functions\_math.jl](https://codecov.io/gh/invenia/NamedDims.jl/pull/9/diff?src=pr&el=tree#diff-c3JjL2Z1bmN0aW9uc19tYXRoLmps) | `100% <ø> (ø)` | :arrow_up: |
| [src/NamedDims.jl](https://codecov.io/gh/invenia/NamedDims.jl/pull/9/diff?src=pr&el=tree#diff-c3JjL05hbWVkRGltcy5qbA==) | `100% <ø> (ø)` | :arrow_up: |
| [src/wrapper\_array.jl](https://codecov.io/gh/invenia/NamedDims.jl/pull/9/diff?src=pr&el=tree#diff-c3JjL3dyYXBwZXJfYXJyYXkuamw=) | `94.11% <100%> (+8.4%)` | :arrow_up: |
| [src/name\_core.jl](https://codecov.io/gh/invenia/NamedDims.jl/pull/9/diff?src=pr&el=tree#diff-c3JjL25hbWVfY29yZS5qbA==) | `90.19% <75%> (-4.68%)` | :arrow_down: |
| [src/broadcasting.jl](https://codecov.io/gh/invenia/NamedDims.jl/pull/9/diff?src=pr&el=tree#diff-c3JjL2Jyb2FkY2FzdGluZy5qbA==) | `80% <80%> (ø)` | |
| [src/functions\_dims.jl](https://codecov.io/gh/invenia/NamedDims.jl/pull/9/diff?src=pr&el=tree#diff-c3JjL2Z1bmN0aW9uc19kaW1zLmps) | `100% <0%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/invenia/NamedDims.jl/pull/9?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/invenia/NamedDims.jl/pull/9?src=pr&el=footer). Last update [e7d0fbb...ee4dc41](https://codecov.io/gh/invenia/NamedDims.jl/pull/9?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

rofinn: LGTM once the conflicts are resolved. I'm still not a fan of `2ones(2)`, but 🤷‍♂️ 
oxinabox: Ok, well I am not actually going to merge this PR against this #7 branch. So I will rebase it off master once #7 is in.

Patch:
diff --git a/src/NamedDims.jl b/src/NamedDims.jl
index 331fbe7..25c102c 100644
--- a/src/NamedDims.jl
+++ b/src/NamedDims.jl
@@ -7,6 +7,7 @@ export NamedDimsArray, dim
 
 include("name_core.jl")
 include("wrapper_array.jl")
+include("broadcasting.jl")
 include("functions.jl")
 include("functions_dims.jl")
 include("functions_math.jl")
diff --git a/src/broadcasting.jl b/src/broadcasting.jl
new file mode 100644
index 0000000..d6e2a38
--- /dev/null
+++ b/src/broadcasting.jl
@@ -0,0 +1,23 @@
+# See: https://docs.julialang.org/en/v1/manual/interfaces/#man-interfaces-broadcasting-1
+
+Base.BroadcastStyle(::Type{<:NamedDimsArray}) = Broadcast.ArrayStyle{NamedDimsArray}()
+
+function Base.similar(
+    bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{NamedDimsArray}},
+    ::Type{T}
+) where T
+
+    L = broadcasted_names(bc)
+    data = similar(Array{T}, axes(bc))
+    return NamedDimsArray{L}(data)
+end
+
+
+broadcasted_names(bc::Base.Broadcast.Broadcasted) = broadcasted_names(bc.args...)
+function broadcasted_names(a, bs...)
+    a_name = broadcasted_names(a)
+    b_name = broadcasted_names(bs...)
+    combine_names_longest(a_name, b_name)
+end
+broadcasted_names(a::AbstractArray) = names(a)
+broadcasted_names(a) = tuple()
diff --git a/src/functions_math.jl b/src/functions_math.jl
index 5d70e36..898e11b 100644
--- a/src/functions_math.jl
+++ b/src/functions_math.jl
@@ -1,22 +1,5 @@
 # This supports nonbroadcasting math on NamedDimArrays
 
-for op in (:+, :-)
-    @eval function Base.$op(a::NamedDimsArray{A}, b::NamedDimsArray{B}) where {A,B}
-        L = combine_names(A, B)
-        data = $op(parent(a), parent(b))
-        return NamedDimsArray{L}(data)
-    end
-    @eval function Base.$op(a::NamedDimsArray{L}, b::AbstractArray) where {L}
-        data = $op(parent(a), b)
-        return NamedDimsArray{L}(data)
-    end
-    @eval function Base.$op(a::AbstractArray, b::NamedDimsArray{L}) where {L}
-        data = $op(a, parent(b))
-        return NamedDimsArray{L}(data)
-    end
-end
-
-
 # Matrix product
 valid_matmul_dims(a::Tuple{Symbol}, b::Tuple{Vararg{Symbol}}) = true
 function valid_matmul_dims(a::Tuple{Symbol, Symbol}, b::Tuple{Vararg{Symbol}})
diff --git a/src/name_core.jl b/src/name_core.jl
index 09a74a2..fa35271 100644
--- a/src/name_core.jl
+++ b/src/name_core.jl
@@ -128,6 +128,25 @@ function order_named_inds(dimnames::Tuple; named_inds...)
     return inds
 end
 
+"""
+    combine_names(a, b)
+
+Produces the merged set of names for tuples of names `a` and `b`,
+or an error if it is not possibly to combine them.
+Two tuples of names can be combined they are the same length
+and if for each position the names are either the same, or one is a wildcard (`:_`).
+When combining wildcard with non-wildcard the resulting name is the non-wildcard.
+(This is somewhat like the very simplest case of unification in e.g prolog).
+
+For example:
+ - `(:a, :b)` and `(:a, :b)` can be combined to give `(:a, :b)`
+ - similarly: `(:a, :_)` and `(:a, :b)` can also be combined to give `(:a, :b)`
+ - `(:a, :b)` and `(:b, :c)` can not be combined as the names at each position to not match.
+ - `(:a, :b)` and `(:a, :b, :c)` can not be combined as they have different lengths
+
+This is the type of name combination used for binary array operations.
+Where the dimensions of both arrays must be the same.
+"""
 function combine_names(names_a, names_b)
     # 0-Allocations if inputs are the same
     # 0-Allocation, if has a `:_` see  `@btime (()->combine_names((:a, :b), (:a, :_)))()`
@@ -152,6 +171,40 @@ function combine_names(names_a, names_b)
     return compile_time_return_hack(ret)
 end
 
+"""
+    combine_names_longest(a, b)
+
+This is the same as [`combine_names`](@ref), but with the equal length requirement removed.
+It combines the names up to the length of the shortest, and takes the named from the longest
+for the remainder.
+It can also be considered as padding the shorter of the two given tuples of names
+with trailing wildcards (`:_`).
+This is the type of name combination used for broadcating array operations.
+Where the smaller (dimensionally) array is broadcast against the longer, repeating it for
+all entries in the missing trailing dimensions.
+"""
+combine_names_longest(names, ::Tuple{}) = names
+combine_names_longest(::Tuple{}, names) = names
+combine_names_longest(::Tuple{}, ::Tuple{}) = tuple()
+function combine_names_longest(a_names, b_names)
+    # 1 Allocation: @btime (()-> combine_names_longest((:a,:b), (:a,)))()
+
+    length(a_names) == length(b_names) && return combine_names(a_names, b_names)
+    long, short = length(a_names) > length(b_names) ? (a_names, b_names) : (b_names, a_names)
+    short_names = identity_namedtuple(short)
+    return ntuple(length(long)) do ii
+        a = getfield(long, ii)
+        b = get(short_names, ii, :_)
+        a === :_ && return b
+        b === :_ && return a
+        a === b && return a
+
+        err_msg = "Attempted to combine arrays with incompatible dimension names. "
+        throw(DimensionMismatch(err_msg * "$a_names ≠ $b_names."))
+    end
+end
+
+
 """
     remaining_dimnames_from_indexing(dimnames::Tuple, inds...)
 Given a tuple of dimension names
diff --git a/src/wrapper_array.jl b/src/wrapper_array.jl
index 744a7bf..89df970 100644
--- a/src/wrapper_array.jl
+++ b/src/wrapper_array.jl
@@ -1,5 +1,5 @@
 """
-    NamedDimsArray{L,T,N,A}(data)
+    NamedDimsArray{L, T, N, A}(data)
 
 A `NamedDimsArray` is a wrapper array type, that provides a view onto  the
 orignal array, which can have its dimensions refer to name rather than by
@@ -20,13 +20,13 @@ x = x[observations=15, features=2]  # 2nd feature in 15th observation.
 They generally resolve most dimension name related operations at compile
 time.
 """
-struct NamedDimsArray{L, T, N, A<:AbstractArray{T,N}} <: AbstractArray{T,N}
+struct NamedDimsArray{L, T, N, A<:AbstractArray{T, N}} <: AbstractArray{T, N}
     # `L` is for labels, it should be an `NTuple{N, Symbol}`
     data::A
 end
 
 
-function NamedDimsArray{L}(orig::AbstractArray{T,N}) where {L, T, N}
+function NamedDimsArray{L}(orig::AbstractArray{T, N}) where {L, T, N}
     if !(L isa NTuple{N, Symbol})
         throw(ArgumentError(
             "A $N dimentional array, needs a $N-tuple of dimension names. Got: $L"
@@ -34,11 +34,11 @@ function NamedDimsArray{L}(orig::AbstractArray{T,N}) where {L, T, N}
     end
     return NamedDimsArray{L, T, N, typeof(orig)}(orig)
 end
-function NamedDimsArray(orig::AbstractArray{T,N}, names::NTuple{N, Symbol}) where {T, N}
+function NamedDimsArray(orig::AbstractArray{T, N}, names::NTuple{N, Symbol}) where {T, N}
     return NamedDimsArray{names}(orig)
 end
 
-parent_type(::Type{<:NamedDimsArray{L,T,N,A}}) where {L,T,N,A} = A
+parent_type(::Type{<:NamedDimsArray{L, T, N, A}}) where {L, T, N, A} = A
 Base.parent(x::NamedDimsArray) = x.data
 
 
@@ -48,7 +48,7 @@ Base.parent(x::NamedDimsArray) = x.data
 Returns a tuple of containing the names of all the dimensions of the array `A`.
 """
 names(::Type{<:NamedDimsArray{L}}) where L = L
-names(::Type{<:AbstractArray{T,N}}) where {T,N} = ntuple(_->:_, N)
+names(::Type{<:AbstractArray{T, N}}) where {T,N} = ntuple(_->:_, N)
 names(x::T) where T<:AbstractArray = names(T)
 
 dim(a::NamedDimsArray{L}, name) where L = dim(L, name)
@@ -73,8 +73,38 @@ Base.axes(a::NamedDimsArray) = axes(parent(a))
 Base.axes(a::NamedDimsArray, d) = axes(parent(a), dim(a, d))
 
 
-function Base.similar(a::NamedDimsArray{L}, args::Type...) where L
-    return NamedDimsArray{L}(similar(parent(a), args...))
+function named_size(a::AbstractArray{T,N}) where {T,N}
+    L = names(a)
+    NamedTuple{L, NTuple{N,Int}}(size(a))
+end
+function Base.similar(
+    a::NamedDimsArray{L,T},
+    eltype::Type=T,
+    dims::NamedTuple{new_names}=named_size(a)
+) where {L,T,new_names}
+
+    new_sizes = values(dims)
+    return NamedDimsArray{new_names}(similar(parent(a), eltype, new_sizes))
+end
+
+function Base.similar(
+    a::NamedDimsArray{L, T, N},
+    eltype::Type,
+    new_names::NTuple{N, Symbol}
+) where {T,N,L}
+    dims = NamedTuple{new_names, NTuple{N, Int}}(size(a))
+    return similar(a, eltype, dims)
+end
+
+
+function Base.similar(
+    a::NamedDimsArray{L, T, N},
+    eltype::Type,
+    new_sizes::NTuple{N, Int}
+) where {L,T,N}
+
+    dims = NamedTuple{L, NTuple{N, Int}}(new_sizes)
+    return similar(a, eltype, dims)
 end
 
 
@@ -117,9 +147,9 @@ for f in (:getindex, :view, :dotview)
             return Base.$f(parent(a), inds...)
         end
 
-        @propagate_inbounds function Base.$f(a::NamedDimsArray, ind::CartesianIndex)
+        @propagate_inbounds function Base.$f(a::NamedDimsArray, ci::CartesianIndex)
             # Easy scalar case, will just return the element
-            return Base.$f(parent(a), ind)
+            return Base.$f(parent(a), ci)
         end
 
         @propagate_inbounds function Base.$f(a::NamedDimsArray, inds...)


Test patch:
diff --git a/test/broadcasting.jl b/test/broadcasting.jl
new file mode 100644
index 0000000..6afaf12
--- /dev/null
+++ b/test/broadcasting.jl
@@ -0,0 +1,83 @@
+using NamedDims
+using NamedDims: names
+using Test
+
+@testset "Binary broadcasting operations (.+)" begin
+    nda = NamedDimsArray{(:a,)}(ones(3))
+
+    @testset "standard case" begin
+        @test nda .+ nda == 2ones(3)
+        @test names(nda .+ nda) == (:a,)
+
+        @test nda .+ nda .+ nda == 3ones(3)
+        @test names(nda .+ nda .+ nda) == (:a,)
+    end
+
+    @testset "partially named dims" begin
+        ndx = NamedDimsArray{(:x, :_)}(ones(3, 5))
+        ndy = NamedDimsArray{(:_, :y)}(ones(3, 5))
+
+        lhs = ndx .+ ndy
+        rhs = ndy .+ ndx
+        @test names(lhs) == (:x, :y) == names(rhs)
+        @test lhs == 2ones(3, 5) == rhs
+    end
+
+    @testset "Dimension disagreement" begin
+        @test_throws DimensionMismatch .+(
+            NamedDimsArray{(:a, :b, :c, :d)}(zeros(3, 3, 3, 3)),
+            NamedDimsArray{(:w, :x, :y, :z)}(ones(3, 3, 3, 3))
+        )
+    end
+
+    @testset "named and unnamed" begin
+        lhs_sum = .+(
+            NamedDimsArray{(:a,:b,:c,:d)}(zeros(3, 3, 3, 3)),
+            ones(3, 3, 3, 3)
+        )
+        @test lhs_sum == ones(3, 3, 3, 3)
+        @test names(lhs_sum) == (:a, :b, :c, :d)
+
+
+        rhs_sum = .+(
+            zeros(3, 3, 3, 3),
+            NamedDimsArray{(:w, :x, :y, :z)}(ones(3, 3, 3, 3))
+        )
+        @test rhs_sum == ones(3, 3, 3, 3)
+        @test names(rhs_sum) == (:w, :x, :y, :z)
+    end
+
+    @testset "broadcasting" begin
+        v = NamedDimsArray{(:time,)}(zeros(3,))
+        m = NamedDimsArray{(:time, :value)}(ones(3, 3))
+        s = 0
+
+        @test v .+ m == ones(3, 3) == m .+ v
+        @test s .+ m == ones(3, 3) == m .+ s
+        @test s .+ v .+ m == ones(3, 3) == m .+ s .+ v
+
+        @test names(v .+ m) == (:time, :value) == names(m .+ v)
+        @test names(s .+ m) == (:time, :value) == names(m .+ s)
+        @test names(s .+ v .+ m) == (:time, :value) == names(m .+ s .+ v)
+    end
+
+    @testset "Mixed array types" begin
+        casts = (
+            NamedDimsArray{(:foo, :bar)},  # Named Matrix
+            x->NamedDimsArray{(:foo,)}(x[:, 1]),  # Named Vector
+            x->NamedDimsArray{(:foo, :bar)}(x[:, 1:1]),  # Named Single Column Matrix
+            identity, # Matrix
+            x->x[:, 1], # Vector
+            x->x[:, 1:1], # Single Column Matrix
+            first, # Scalar
+         )
+        for (T1, T2, T3) in Iterators.product(casts, casts, casts)
+            all(isequal(identity), (T1, T2, T3)) && continue
+            !any(isequal(NamedDimsArray{(:foo, :bar)}), (T1, T2, T3)) && continue
+
+            total = T1(ones(3, 6)) .+ T2(2ones(3, 6)) .+ T3(3ones(3, 6))
+            @test total == 6ones(3, 6)
+            @test names(total) == (:foo, :bar)
+        end
+    end
+end
diff --git a/test/functions_dims.jl b/test/functions_dims.jl
index 53c3fd4..b40664f 100644
--- a/test/functions_dims.jl
+++ b/test/functions_dims.jl
@@ -3,16 +3,16 @@ using NamedDims: names
 using Test
 
 @testset "dropdims" begin
-    nda = NamedDimsArray{(:a, :b, :c, :d)}(ones(10,1,1,20))
+    nda = NamedDimsArray{(:a, :b, :c, :d)}(ones(10, 1, 1, 20))
 
     @test_throws ArgumentError dropdims(nda; dims=1)
     @test_throws ArgumentError dropdims(nda; dims=:a)
 
-    @test dropdims(nda; dims=:b) == ones(10,1,20) == dropdims(nda; dims=2)
+    @test dropdims(nda; dims=:b) == ones(10, 1, 20) == dropdims(nda; dims=2)
     @test names(dropdims(nda; dims=:b)) == (:a, :c, :d) == names(dropdims(nda; dims=2))
 
-    @test dropdims(nda; dims=(:b,:c)) == ones(10,20) == dropdims(nda; dims=(2,3))
-    @test names(dropdims(nda; dims=(:b,:c))) == (:a, :d) == names(dropdims(nda; dims=(2,3)))
+    @test dropdims(nda; dims=(:b,:c)) == ones(10, 20) == dropdims(nda; dims=(2, 3))
+    @test names(dropdims(nda; dims=(:b, :c))) == (:a, :d) == names(dropdims(nda; dims=(2, 3)))
 end
 
 @testset "$f" for f in (adjoint, transpose, permutedims)
diff --git a/test/functions_math.jl b/test/functions_math.jl
index 71387a9..e5765a8 100644
--- a/test/functions_math.jl
+++ b/test/functions_math.jl
@@ -1,5 +1,5 @@
 using NamedDims
-using NamedDims: matrix_prod_names
+using NamedDims: matrix_prod_names, names
 using Test
 
 @testset "+" begin
@@ -9,57 +9,57 @@ using Test
         @test +(nda) == ones(3)
         @test names(+(nda)) == (:a,)
 
-        @test +(nda, nda) == 2*ones(3)
+        @test +(nda, nda) == 2ones(3)
         @test names(+(nda, nda)) == (:a,)
 
-        @test +(nda, nda, nda) == 3*ones(3)
+        @test +(nda, nda, nda) == 3ones(3)
         @test names(+(nda, nda, nda)) == (:a,)
     end
 
     @testset "partially named dims" begin
-        ndx = NamedDimsArray{(:x,:_)}(ones(3,5))
-        ndy = NamedDimsArray{(:_,:y)}(ones(3,5))
+        ndx = NamedDimsArray{(:x, :_)}(ones(3, 5))
+        ndy = NamedDimsArray{(:_, :y)}(ones(3, 5))
 
         lhs = ndx + ndy
         rhs = ndy + ndx
         @test names(lhs) == (:x, :y) == names(rhs)
-        @test lhs == 2*ones(3,5) == rhs
+        @test lhs == 2ones(3, 5) == rhs
     end
 
     @testset "Dimension disagreement" begin
         @test_throws DimensionMismatch +(
-            NamedDimsArray{(:a,:b,:c,:d)}(zeros(3,3,3,3)),
-            NamedDimsArray{(:w,:x,:y,:z)}(ones(3,3,3,3))
+            NamedDimsArray{(:a, :b, :c, :d)}(zeros(3, 3, 3, 3)),
+            NamedDimsArray{(:w, :x, :y, :z)}(ones(3, 3, 3, 3))
         )
 
         @test_throws DimensionMismatch +(
             NamedDimsArray{(:time,)}(zeros(3,)),
-            NamedDimsArray{(:time, :value)}(ones(3,3))
+            NamedDimsArray{(:time, :value)}(ones(3, 3))
         )
     end
 
     @testset "Mixed array types" begin
         lhs_sum = +(
-            NamedDimsArray{(:a,:b,:c,:d)}(zeros(3,3,3,3)),
-            ones(3,3,3,3)
+            NamedDimsArray{(:a, :b, :c, :d)}(zeros(3, 3, 3, 3)),
+            ones(3, 3, 3, 3)
         )
-        @test lhs_sum == ones(3,3,3,3)
-        @test names(lhs_sum) == (:a,:b,:c,:d)
+        @test lhs_sum == ones(3, 3, 3, 3)
+        @test names(lhs_sum) == (:a, :b, :c, :d)
 
 
         rhs_sum = +(
-            zeros(3,3,3,3),
-            NamedDimsArray{(:w,:x,:y,:z)}(ones(3,3,3,3))
+            zeros(3, 3, 3, 3),
+            NamedDimsArray{(:w, :x, :y, :z)}(ones(3, 3, 3, 3))
         )
-        @test rhs_sum == ones(3,3,3,3)
-        @test names(rhs_sum) == (:w,:x,:y,:z)
+        @test rhs_sum == ones(3, 3, 3, 3)
+        @test names(rhs_sum) == (:w, :x, :y, :z)
 
 
         casts = (NamedDimsArray{(:foo, :bar)}, identity)
-        for (T1,T2,T3,T4) in Iterators.product(casts, casts, casts, casts)
-            all(isequal(identity), (T1,T2,T3,T4)) && continue
-            total = T1(ones(3,6)) + T2(2*ones(3,6)) + T3(3*ones(3,6)) + T4(4*ones(3,6))
-            @test total == 10*ones(3,6)
+        for (T1, T2, T3, T4) in Iterators.product(casts, casts, casts, casts)
+            all(isequal(identity), (T1, T2, T3, T4)) && continue
+            total = T1(ones(3, 6)) + T2(2ones(3, 6)) + T3(3ones(3, 6)) + T4(4ones(3, 6))
+            @test total == 10ones(3, 6)
             @test names(total) == (:foo, :bar)
         end
     end
@@ -69,12 +69,18 @@ end
 @testset "-" begin
     # This is actually covered by the tests for + above, since that uses the same code
     # just one extra as a sensability check
-    nda = NamedDimsArray{(:a,:b)}(ones(3,100))
+    nda = NamedDimsArray{(:a, :b)}(ones(3, 100))
     @test nda - nda == zeros(3, 100)
     @test names(nda - nda) == (:a, :b)
 end
 
 
+@testset "scalar product" begin
+    nda = NamedDimsArray{(:a, :b, :c, :d, :e)}(ones(10, 20, 30, 40, 50))
+    @test 10nda == 10ones(10, 20, 30, 40, 50)
+    @test names(10nda) == (:a, :b, :c, :d, :e)
+end
+
 
 @testset "matmul" begin
     @testset "matrix_prod_names" begin
@@ -96,18 +102,18 @@ end
     end
 
     @testset "Matrix-Matrix" begin
-        nda = NamedDimsArray{(:a,:b)}(ones(2,3))
-        ndb = NamedDimsArray{(:b,:c)}(ones(3,2))
+        nda = NamedDimsArray{(:a, :b)}(ones(2, 3))
+        ndb = NamedDimsArray{(:b, :c)}(ones(3, 2))
 
         @testset "standard case" begin
-            @test nda * ndb == 3*ones(2,2)
-            @test names(nda * ndb) == (:a,:c)
+            @test nda * ndb == 3ones(2, 2)
+            @test names(nda * ndb) == (:a, :c)
 
-            @test ones(4,3) * ndb == 3*ones(4,2)
-            @test names(ones(4,3) * ndb) == (:_,:c)
+            @test ones(4, 3) * ndb == 3ones(4, 2)
+            @test names(ones(4, 3) * ndb) == (:_, :c)
 
-            @test nda * ones(3,7) == 3*ones(2,7)
-            @test names(nda * ones(3,7)) == (:a,:_)
+            @test nda * ones(3, 7) == 3ones(2, 7)
+            @test names(nda * ones(3,7)) == (:a, :_)
         end
 
         @testset "Dimension disagreement" begin
@@ -116,7 +122,7 @@ end
     end
 
     @testset "Matrix-Vector" begin
-        ndm = NamedDimsArray{(:a,:b)}(ones(1,1))
+        ndm = NamedDimsArray{(:a, :b)}(ones(1, 1))
         ndv = NamedDimsArray{(:b, )}(ones(1))
 
         @test ndm * ndv == ones(1)
@@ -124,10 +130,10 @@ end
     end
 
     @testset "Vector-Matrix" begin
-        ndm = NamedDimsArray{(:a,:b)}(ones(1,1))
+        ndm = NamedDimsArray{(:a, :b)}(ones(1, 1))
         ndv = NamedDimsArray{(:a, )}(ones(1))
 
-        @test ndv * ndm == ones(1,1)
-        @test names(ndv * ndm) == (:a,:b)
+        @test ndv * ndm == ones(1, 1)
+        @test names(ndv * ndm) == (:a, :b)
     end
 end
diff --git a/test/name_core.jl b/test/name_core.jl
index ef0e265..0aba361 100644
--- a/test/name_core.jl
+++ b/test/name_core.jl
@@ -2,6 +2,7 @@ using NamedDims
 using NamedDims:
     names,
     combine_names,
+    combine_names_longest,
     order_named_inds,
     permute_dimnames,
     remaining_dimnames_from_indexing,
@@ -19,38 +20,48 @@ using Test
         @test values(namemap) == Tuple(1:length(manynames))
     end
     @testset "small case" begin
-        @test dim((:x, :y), :x)==1
-        @test dim((:x, :y), :y)==2
-        @test dim((:x, :y), :z)==0  # not found
+        @test dim((:x, :y), :x) == 1
+        @test dim((:x, :y), :y) == 2
+        @test dim((:x, :y), :z) == 0  # not found
     end
     @testset "large case" begin
-        @test dim((:x, :y, :a, :b, :c, :d), :x)==1
-        @test dim((:x, :y, :a, :b, :c, :d), :a)==3
-        @test dim((:x, :y, :a, :b, :c, :d), :d)==6
-        @test dim((:x, :y, :a, :b, :c, :d), :z)==0 # not found
+        @test dim((:x, :y, :a, :b, :c, :d), :x) == 1
+        @test dim((:x, :y, :a, :b, :c, :d), :a) == 3
+        @test dim((:x, :y, :a, :b, :c, :d), :d) == 6
+        @test dim((:x, :y, :a, :b, :c, :d), :z) == 0 # not found
     end
 end
 
-@testset "combine_names" begin
-    @test combine_names((:a,), (:a,)) == (:a,)
-    @test combine_names((:a,:b), (:a,:b)) == (:a,:b)
-    @test combine_names((:a,:_), (:a,:b)) == (:a,:b)
-    @test combine_names((:a,:_), (:a,:_)) == (:a,:_)
+@testset "combine_names/combine_names_longest" begin
+    @test_throws DimensionMismatch combine_names((:a,), (:a, :b,))
 
-    @test combine_names((:a,:b,:c), (:_,:_,:_)) == (:a,:b,:c)
-    @test combine_names((:a,:_,:c), (:_,:b,:_)) == (:a,:b,:c)
-    @test combine_names((:_,:_,:_), (:_,:_,:_)) == (:_,:_,:_)
+    @test combine_names_longest((:a,), (:a, :b,)) == (:a, :b)
+    @test combine_names_longest((:a,), (:a, :_)) == (:a, :_)
+    @test combine_names_longest((:a, :b), (:a, :_, :c)) == (:a, :b, :c)
 
-    @test_throws DimensionMismatch combine_names((:a,), (:b,))
-    @test_throws DimensionMismatch combine_names((:a,), (:a, :b,))
-    @test_throws DimensionMismatch combine_names((:a,:b,:c), (:_,:_,:d))
+    @test_throws DimensionMismatch combine_names_longest((:a, :b, :c), (:b, :a))
+
+    for combine in (combine_names, combine_names_longest)
+        @test combine((:a,), (:a,)) == (:a,)
+        @test combine((:a, :b), (:a, :b)) == (:a, :b)
+        @test combine((:a, :_), (:a, :b)) == (:a, :b)
+        @test combine((:a, :_), (:a, :_)) == (:a, :_)
+
+        @test combine((:a, :b, :c), (:_, :_, :_)) == (:a, :b, :c)
+        @test combine((:a, :_, :c), (:_, :b, :_)) == (:a, :b, :c)
+        @test combine((:_, :_, :_), (:_, :_, :_)) == (:_, :_, :_)
+
+        @test_throws DimensionMismatch combine((:a,), (:b,))
+        @test_throws DimensionMismatch combine((:a,:b), (:b, :a))
+        @test_throws DimensionMismatch combine((:a, :b, :c), (:_, :_, :d))
+    end
 end
 
 @testset "order_named_inds" begin
     @test order_named_inds((:x,)) == (:,)
     @test order_named_inds((:x,); x=2) == (2,)
 
-    @test order_named_inds((:x, :y,)) == (:,:)
+    @test order_named_inds((:x, :y,)) == (:, :)
     @test order_named_inds((:x, :y); x=2) == (2, :)
     @test order_named_inds((:x, :y); y=2, ) == (:, 2)
     @test order_named_inds((:x, :y); y=20, x=30) == (30, 20)
@@ -59,7 +70,7 @@ end
 
 @testset "remaining_dimnames_from_indexing" begin
     @test remaining_dimnames_from_indexing((:a, :b, :c), (10,20,30)) == tuple()
-    @test remaining_dimnames_from_indexing((:a, :b, :c), (10,:,30)) == (:b,)
+    @test remaining_dimnames_from_indexing((:a, :b, :c), (10, :,30)) == (:b,)
     @test remaining_dimnames_from_indexing((:a, :b, :c), (1:1, [true], [20])) == (:a, :b, :c)
 end
 
diff --git a/test/runtests.jl b/test/runtests.jl
index a399fe8..4b04317 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -1,12 +1,17 @@
 using NamedDims
 using Test
 
-@testset "NamedDims.jl" begin
-    # Write your own tests here.
+const testfiles = (
+    "name_core.jl",
+    "wrapper_array.jl",
+    "functions.jl",
+    "functions_dims.jl",
+    "functions_math.jl",
+    "broadcasting.jl",
+)
 
-    include("name_core.jl")
-    include("wrapper_array.jl")
-    include("functions.jl")
-    include("functions_dims.jl")
-    include("functions_math.jl")
+@testset "NamedDims.jl" begin
+    @testset "$file" for file in testfiles
+        include(file)
+    end
 end
diff --git a/test/wrapper_array.jl b/test/wrapper_array.jl
index 5e029c6..127ad7c 100644
--- a/test/wrapper_array.jl
+++ b/test/wrapper_array.jl
@@ -79,8 +79,49 @@ end
     @test length(nda) == 6
 
     @test axes(nda) == (1:3, 1:2)
-    @test axes(nda,:x) == (1:3) == axes(nda,1)
+    @test axes(nda,:x) == (1:3) == axes(nda, 1)
 
     @test size(nda) == (3,2)
-    @test size(nda,:x) == 3 == size(nda,1)
+    @test size(nda,:x) == 3 == size(nda, 1)
+end
+
+@testset "similar" begin
+    nda = NamedDimsArray{(:a, :b, :c, :d)}(ones(10, 20, 30, 40))
+
+    @testset "content" begin
+        ndb = similar(nda)
+        @test parent(ndb) !== parent(nda)
+        @test eltype(ndb) == Float64
+        @test size(ndb) == (10, 20 ,30, 40)
+        @test names(ndb) == (:a, :b, :c, :d)
+    end
+    @testset "eltype" begin
+        ndb = similar(nda, Char)
+        @test parent(ndb) !== parent(nda)
+        @test eltype(ndb) == Char
+        @test size(ndb) == (10, 20, 30, 40)
+        @test names(ndb) == (:a, :b, :c, :d)
+    end
+    @testset "size" begin
+        ndb = similar(nda, Float64, (15, 25, 35, 45))
+        @test parent(ndb) !== parent(nda)
+        @test eltype(ndb) == Float64
+        @test size(ndb) == (15, 25, 35, 45)
+        @test names(ndb) == (:a, :b, :c, :d)
+    end
+    @testset "dim names" begin
+        ndb = similar(nda, Float64, (:w, :x, :y, :z))
+        @test parent(ndb) !== parent(nda)
+        @test eltype(ndb) == Float64
+        @test size(ndb) == (10, 20, 30, 40)
+        @test names(ndb) == (:w, :x, :y, :z)
+    end
+
+    @testset "dimensions" begin
+        ndb = similar(nda, Float64, (w=11, x=22))
+        @test parent(ndb) !== parent(nda)
+        @test eltype(ndb) == Float64
+        @test size(ndb) == (11,22)
+        @test names(ndb) == (:w, :x,)
+    end
 end