Skip to content

Non-dispatchable field specialization syntax. #45687

@ChrisRackauckas

Description

@ChrisRackauckas

I was looking again at #40138 and my more evolved understanding of this issue is the fact that fully parametric fields are treated in the same manner as other type parameters, and that just seems wrong. For example, if I have a type

struct MyType{T}
  x::T
end

What our current stacktrace assumes is that knowing T is "fundamental" to understanding MyType. In reality, I only did ::T because I wanted the Julia compiler to optimize it. If it wasn't a programming optimization, then I would've done:

struct MyType
  x
end

There's nothing fundamental to understanding the type of x that is required for understanding how MyType works: nothing ever dispatches on it, it's not part of the public API (so I don't intend anyone to ever dispatch on it), yet it's treated as first-class information. How can I allow program specialization here without allowing ease of dispatch? I can't. This seems to be a fundamentally missing feature in the type system.

For an example of this in practice, look at none other than the infamous ODEIntegrator type.

mutable struct ODEIntegrator{algType<:Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm},IIP,uType,duType,tType,pType,eigenType,EEstT,QT,tdirType,ksEltype,SolType,F,CacheType,O,FSALType,EventErrorType,CallbackCacheType,IA} <: DiffEqBase.AbstractODEIntegrator{algType,IIP,uType,tType}
  sol::SolType
  u::uType
  du::duType
  k::ksEltype
  t::tType
  dt::tType
  f::F
  p::pType
  uprev::uType
  uprev2::uType
  duprev::duType
  tprev::tType
  alg::algType
  dtcache::tType
  dtchangeable::Bool
  dtpropose::tType
  tdir::tdirType
  eigen_est::eigenType
  EEst::EEstT
  qold::QT
  q11::QT
  erracc::QT
  dtacc::tType
  success_iter::Int
  iter::Int
  saveiter::Int
  saveiter_dense::Int
  cache::CacheType
  callback_cache::CallbackCacheType
  kshortsize::Int
  force_stepfail::Bool
  last_stepfail::Bool
  just_hit_tstop::Bool
  do_error_check::Bool
  event_last_time::Int
  vector_event_last_time::Int
  last_event_error::EventErrorType
  accept_step::Bool
  isout::Bool
  reeval_fsal::Bool
  u_modified::Bool
  reinitialize::Bool
  isdae::Bool
  opts::O
  destats::DiffEqBase.DEStats
  initializealg::IA
  fsalfirst::FSALType
  fsallast::FSALType
end

where one of its fields are:

mutable struct DEOptions{absType,relType,QT,tType,Controller,F1,F2,F3,F4,F5,F6,F7,tstopsType,discType,ECType,SType,MI,tcache,savecache,disccache}
  maxiters::MI
  save_everystep::Bool
  adaptive::Bool
  abstol::absType
  reltol::relType
  gamma::QT
  qmax::QT
  qmin::QT
  qsteady_max::QT
  qsteady_min::QT
  qoldinit::QT
  failfactor::QT
  dtmax::tType
  dtmin::tType
  controller::Controller
  internalnorm::F1
  internalopnorm::F2
  save_idxs::SType
  tstops::tstopsType
  saveat::tstopsType
  d_discontinuities::discType
  tstops_cache::tcache
  saveat_cache::savecache
  d_discontinuities_cache::disccache
  userdata::ECType
  progress::Bool
  progress_steps::Int
  progress_name::String
  progress_message::F6
  timeseries_errors::Bool
  dense_errors::Bool
  dense::Bool
  save_on::Bool
  save_start::Bool
  save_end::Bool
  save_end_user::F3
  callback::F4
  isoutofdomain::F5
  unstable_check::F7
  verbose::Bool
  calck::Bool
  force_dtmin::Bool
  advance_to_tstop::Bool
  stop_at_next_tstop::Bool
end

Beautiful. This is the thing that generated a literally 10,000 character stack trace when Unitful numbers were put into it. But why? Let's understand this in the context of the above.

  abstol::absType
  reltol::relType

abstol and reltol are parametrically typed because it's possible for a user to pass different types in there. Yes it's normal to solve(prob,Tsit5(),abstol=1e-6,reltol=1e-6), but you can also do solve(prob,Tsit5(),abstol=[1e-6,1e-2],reltol=1e-6) to have per-element tolerances. Great feature right? Well by allowing this feature, I now have to make the internal struct parametric, and so now everybody who uses the ODE solver has to see absType and relType in every stack trace. Mind you, not a single function in the entire library dispatches on this type parameter, nor is anything ever intended to, so this type parameter is a waste of time in any stacktrace: I've just learned to ignore it.

The integrator type has >100 type parameters at any given type, and the first thing we have to train a user to do is ignore all of them. What the hell? Don't we think something has gone very wrong here?

I think the issue is that we have in the type syntax conflated the different ideas of "I want this program to specialize on this field" and "I want to be able to dispatch on this field". When we write functions, we do f(x) and we know that f will (most likely, some heuristics) specialize on the type of x. We do not require that people write f(x::T) where T everywhere. If we did, our function stack traces would be ginormous because every call would describe methods like f(x::T,y::T2,z::T3) where {T1.T2.T3} and people would go "what does this mean?". But we do not treat types the same way: we force basically "where T" on every field that we want to specialize, and we print this information to the user that basically just say "hey, DifferentialEquations.jl likes to specialize, you wanted to know that right?"

No, nobody cares. So what can we do? What about a new language feature for non-dispatchable field specialization syntax?

mutable struct DEOptions{tType}
  maxiters::Specialize
  save_everystep::Bool
  adaptive::Bool
  abstol::Specialize
  reltol::Specialize
  gamma::Specialize
  qmax::Specialize
  qmin::Specialize
  qsteady_max::Specialize
  qsteady_min::Specialize
  qoldinit::Specialize
  failfactor::Specialize
  dtmax::tType
  dtmin::tType
  controller::Specialize
  internalnorm::Specialize
  internalopnorm::Specialize
  save_idxs::Specialize
  tstops::Specialize
  saveat::Specialize
  d_discontinuities::Specialize
  tstops_cache::Specialize
  saveat_cache::Specialize
  d_discontinuities_cache::Specialize
  userdata::Specialize
  progress::Bool
  progress_steps::Int
  progress_name::String
  progress_message::Specialize
  timeseries_errors::Bool
  dense_errors::Bool
  dense::Bool
  save_on::Bool
  save_start::Bool
  save_end::Bool
  save_end_user::Specialize
  callback::Specialize
  isoutofdomain::Specialize
  unstable_check::Specialize
  verbose::Bool
  calck::Bool
  force_dtmin::Bool
  advance_to_tstop::Bool
  stop_at_next_tstop::Bool
end

And it would then be really nice to have a macro in Base where this is equivalent to:

@specialize mutable struct DEOptions{tType}
  maxiters
  save_everystep::Bool
  adaptive::Bool
  abstol
  reltol
  gamma
  qmax
  qmin
  qsteady_max
  qsteady_min
  qoldinit
  failfactor
  dtmax::tType
  dtmin::tType
  controller
  internalnorm
  internalopnorm
  save_idxs
  tstops
  saveat
  d_discontinuities
  tstops_cache
  saveat_cache
  d_discontinuities_cache
  userdata
  progress::Bool
  progress_steps::Int
  progress_name::String
  progress_message
  timeseries_errors::Bool
  dense_errors::Bool
  dense::Bool
  save_on::Bool
  save_start::Bool
  save_end::Bool
  save_end_user
  callback
  isoutofdomain
  unstable_check
  verbose::Bool
  calck::Bool
  force_dtmin::Bool
  advance_to_tstop::Bool
  stop_at_next_tstop::Bool
end

This better captures the essence of what the type is, makes it easier to write code without performance bugs, and will go a very long way to making the stacktraces more legible.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions