Need feedback on syntax design

Hey there, I’m working on a new library that allows you to define CollectionService objects in an extremely declarative manner, I’m asking for syntax design questions.

The underlying library itself is finished and works amazingly (not battle tested but it mostly works), this is more of a library ontop of it to provide a declarative design. The library itself allows you to make objects with CollectionService tags, or completely headerless objects if you just want the maid stuff related to threads and events.

The first syntax design I’m going for is this, feel free to give feedback on it!

Most syntax here is commented as its not relevent to the implementation shown

return Object("KillBrick", "BasePart") { -- The first parameter here defines which tag to hook to, and the second parameter is a class name filter through IsA
  --Init = function(self) - signalled when the object is set up but after everything else is done
  --Destroying = function(self) -- signalled when the object is deleted, (CS tag removed)
  --ComponentMoved = function(self, oldParent) -- for specifically components, fired when a component changes parent

  Touched = function(self, sender, otherPart) -- this will define the event Touched (which all BaseParts implement, self refers to the object, and sender refers to the part, event parameters are then send variadically, here as otherPart
    
  end
  -- Special logic can be added as keys, the following of which are below
  -- A key here refers to a function that returns a special table, ie: Change "Size"
  -- Most take one to two options, extra ie Attribute("Specialness", "ClickDetector")

  -- [Change (PropertyName, Path?)] - Property Change (self, sender, name, newValue) (its not possible to track the old value cheaply)
  -- [StateChange (StateName)] - Internal state change (self, sender, name, newValue, oldValue)
  -- [AttributeChange (PropertyName, Path?)] - Instance attribute change (self, sender, name, newValue), unlike StateChange we could track old values here but performce concerns wrt instances unrelated to the base instance.
  -- [Event (RBXScriptConnection, Path)] - external event definition, RBXScriptSignal cant be used because the sender needs to be derived, and signals cant do that directly. (self, sender, eventArgs...)
}

You may also refer to members of the instance with a simple path syntax:

["ClickDetector.MouseClick"] = function(...)
  ...

For questions about the special keys, please follow me up, but feel free to give support on any of this.

1 Like

Rather than a ClassName filter, I could instead narrow like this, it gives you more freedom and control with how an object is connected:

Instance here refers to the instance or the component’s parent instance.

{
  ObjectFilter = function(instance)
    return instance:IsA("BasePart")
  end
}

This would be useful if you want to, for example, constrain a component to a single service:

  ObjectFilter = function(instance)
    return instance:IsA("Players")
  end

so would your first design do this?

-- assuming: type Object = (Tag: string, ClassName: string) -> ((...any) -> (Instance))
-- I prefer '.new' (similar to Instance.new)
local MoneyGiver = Object.new("MoneyGiver", "Part") {
  ["Init"] = function(self)
    -- How would i set properties on init?
    self.Color = Color3.fromRGB(255, 255, 0)
    self.Material = Enum.Material.Neon
    self.Name = "MoneyGiver"
    self:SetAttribute("MoneyIncrement", 9999)
    self.Anchored = true
    self.Size = Vector3.new(8, 10, 8)
    self.Position = Vector3.new(0, 10, 0)
    self.Parent = workspace
    print("Created a MoneyGiver block")
  end;

  ["Touched"] = function(self, Sender, Hit)
    local Player = Players:GetPlayerFromCharacter(Hit.Parent)
    if not Player then return end
    
    local MoneyIncrement = Sender:GetAttribute("MoneyIncrement") or 10
    print(Player.Name, "touched a MoneyGiver block and got", MoneyIncrement, "money!")
    Player.leaderstats.Money.Value += MoneyIncrement
  end;
}

image

image

This creates collection service based logic for objects that already exist, the second parameter is a class name filter for restricting what can implement the tag, as well as that it has some memory ownership stuff so you dont have to worry about threads not dying when you destroy the object.

Yes you can fundamentally do this with it but thats not the goal here.