2025_06_12_Code

require './tp.rb' 

tp = Tp.new

# set-up some types
tp.rules {
  rule :Bool, [TrueClass, FalseClass]
  rule :Nil, [NilClass]
  rule :Num, [Numeric.subclasses]
  rule :Enum, ->(x) {x.is_a? Enumerable}
  rule :Container, [Hash, Array, Set, Struct, Data]
  rule :Int, [Integer]
  rule :Dec, [Float]
  rule :Rat, [Rational]
  rule :StrLike, [String, Symbol, Regexp]
  rule :Str, [String]
}


# -------------------------------------
# Can be used to check variables
p tp. (3 == 3), :Bool # true
p tp. '1/5'.to_r, :Num # true
p tp. [1,2,3], :Enum # true

p tp. nil, :Num, :Nil # true

# '13' can't be :Num or :Bool
p tp. '13', *%i(Num Bool), negate: true # true

# -------------------------------------
# Can be used to prevent variables being
# assigned certain "types".. or allowing
# just some type(s).
name = 'Ior'.then { it if tp.(it, :StrLike) }
p name # "Ior"

name2 = /Ukraine/.then { it unless tp.(it, :StrLike, negate: true) }
p name2 # /Ukraine/

# -------------------------------------
# Can be used to assign either-or
name3 = 12.then { tp.(it, :StrLike) ? it : :Ior }
p name3 # :Ior

# -------------------------------------
# Can be use to set other constraints on values
tp.rules {
  rule :Limit, ->(x) { x.even? && x.between?(2, 20) }
}




# Nicen it up a bit
set_if = ->(var, *types, negate: false) { var if tp.(var, *types) }

nn = set_if.('Ior', :Numeric)

nn = set_if.('Ior', :StrLike)
p nn # "Ior"


until rand_val ||= set_if.(rand(100), :Limit); end
p rand_val # 16 (example)

tp.rb

class Tp

  def initialize = ( @tp = {} )

  def rules(&bl) = ( instance_exec(&bl) )

  def (val, *tps, negate: false)
    tps.map { check(it, val) }.send(negate ? :none? : :any?)
  end

  private
  
  def rule(key, val) = ( @tp[key] = val )

  def check(tp, val)
    if @tp.key?(tp)
      case res = @tp[tp]
      when Array then res.flatten.include?(val.class)
      when Proc then res.(val)
      end
    end
  end

end