Skip to content

Generalize Base.Fix1 and Base.Fix2 #36181

@goretkin

Description

@goretkin

Fix1 and Fix2 are, as far as I can tell, intended to be used internally. I think these types can be extremely useful for some APIs and I think they should be generalized and given elevated status. In addition to being useful in the same places as Fix1 and Fix2 are used in Base, by binding some of the arguments, it allows encoding some symbolic manipulation rules in terms of other operations by binding all the arguments.

Here's a possible implementation:

using Base: tail
interleave(bind::Tuple{}, args::Tuple{}) = ()
interleave(bind::Tuple{}, args::Tuple) = error("more args than positions")
interleave(bind, args) = _interleave(first(bind), tail(bind), args)

# `nothing` indicates a position to be bound
_interleave(firstbind::Nothing, tailbind::Tuple, args::Tuple) = (
  first(args), interleave(tailbind, tail(args))...)

# allow escaping of e.g. `nothing`
_interleave(firstbind::Some{T}, tailbind::Tuple, args::Tuple) where T = (
  something(firstbind), interleave(tailbind, args)...)

_interleave(firstbind::T, tailbind::Tuple, args::Tuple) where T = (
  firstbind, interleave(tailbind, args)...)

struct Bind{F, A}
  f::F
  a::A
end

function (c::Bind)(args...)
  c.f(interleave(c.a, args)...)
end

# for backwards compatibility, and succinctness

const Fix1{F, X} =  Bind{F, Tuple{Some{X}, Nothing}}
const Fix2{F, X} =  Bind{F, Tuple{Nothing, Some{X}}}
Fix1(f, x) = Bind(f, (Some(x), nothing))
Fix2(f, x) = Bind(f, (nothing, Some(x)))

getx(f::Fix1) = something(f.a[1])
getx(f::Fix2) = something(f.a[2])

# should probably be a deprecated: 
getproperty(f::Fix1, s::Symbol) = s === :x ? getx(f) : getfield(f, s)
getproperty(f::Fix2, s::Symbol) = s === :x ? getx(f) : getfield(f, s)

e.g.

is3 = Bind(==, (3, nothing))
isnothing2 = Bind(===, (Some(nothing), nothing)) # demonstrate how to escape `nothing`

seems to generate efficient code:

julia> @code_llvm is3(4)

;  @ /Users/goretkin/projects/julia_scraps/curry2.jl:22 within `Bind'
define i8 @julia_Bind_19023({ { i64 } } addrspace(11)* nocapture nonnull readonly dereferenceable(8), i64) {
top:
; ┌ @ /Users/goretkin/projects/julia_scraps/curry2.jl:3 within `interleave'
; │┌ @ tuple.jl:96 within `first'
; ││┌ @ tuple.jl:24 within `getindex'
     %2 = getelementptr inbounds { { i64 } }, { { i64 } } addrspace(11)* %0, i64 0, i32 0, i32 0
; └└└
; ┌ @ promotion.jl:398 within `=='
   %3 = load i64, i64 addrspace(11)* %2, align 8
   %4 = icmp eq i64 %3, %1
   %5 = zext i1 %4 to i8
; └
  ret i

I made a gist trying to demonstrate the benefits I perceive of this proposal. The first example is about replacing Rational{A, B} with Bind{typeof(/), A, B}. This probably shouldn't actually be done, and it at least serves as an illustration:
https://gist.github.com/goretkin/0d86957dd3279ce9d55993467f872794#file-curry-jl-L47-L58

The second example is about deferring the evaluation of union(1:3, 5:7) and carrying a symbolic representation of it to allow more efficient operations downstream.
https://gist.github.com/goretkin/0d86957dd3279ce9d55993467f872794#file-curry-jl-L65-L79

I tried to see if this change would wreak havoc in Base, but I was not able to test it out: #36180

Related:
https://www.cplusplus.com/reference/functional/bind/
PR #36094 Document Fix1 and Fix2
https://github.com/c42f/Underscores.jl

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureIndicates new feature / enhancement requests

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions