[ARCHIVED] Hierarchy Builder - Lightweight D.S.L

This project is archived. It is also not affiliated with Yarik_superpro

Hierarchy Builder

Github: Here | Docs: Here

We all love instance frameworks for their features. But sometimes we really need the bare minimum from them. And hierarchy builder is exactly that.

local buildInstance = require(HierarchyBuilder)

buildInstance{ Name = "This is";
   Type = "Folder";
   Parent = workspace;

   { Type = "Folder";
      
      _init = function(self:Folder)
         self.Name = "how"
      end;

      { Name = "it looks";
         Type = "Configuration";
      };
   };
}

Preview image

As of writing, the source code only contains 135 lines of code, marking how lightweight it is for the features it provides:

  • Declarativeness
  • Inheritance
  • Reactiveness

Why should you use it?

It’s lightweight, fitting in a single source file without requiring any dependencies. It also can integrate easily within existing projects without any issues. It is also extremely easy to learn.

Its usage cases are unlimited: it can be used for GUIs, character setup, and general-purpose serialization.

Being optimized also allows you to not worry about selectively using it. You only spend time designing the hierarchy itself and don’t worry about the performance consequences.

14 Likes

1.0.1

  • Fixed issue with _init running twice when _count is present

This works great! What id like to see though is a performance comparison between using HierarchyBuilder and just Instance:Clone or using the Instance library, overall pretty cool though

I really couldn’t precisely benchmark my module. But from what I have gotten, I can tell you that it is not much different from individual instance creation.

You can try it to see for yourself:

local t:number
t = os.clock()
--individual
local a = Instance.new("Part")
a.Name = "test"

local g = Instance.new("Folder")
g.Name = "test2"
g.Parent = a

local b = Instance.new("ImageLabel")
b.Name = "asdas"
b.Size = UDim2.fromScale(1,1)
b.Parent = g

a.Parent = workspace

print(os.clock()-t)
task.wait()
t = os.clock()
--buildInstance

buildInstance{ Name = "test";
	Type = "Part";
	Parent = workspace;
	
	{ Type = "Folder";
		Name = "test2";
		
		{ Name = "asdas";
			Type = "ImageLabel";
			Size = UDim2.fromScale(1,1);
		}
	}
}

print(os.clock()-t)

1.1.0

  • Removed unnecessary nil casting
  • Updated the instance table index type to support autocompletion partly

Did a benchmark with the boatbomber program and whilst creating instances manually is fast I suppose this system would be more of a convenience factor.

--[[
This file is for use by Benchmarker (https://boatbomber.itch.io/benchmarker)

|WARNING| THIS RUNS IN YOUR REAL ENVIRONMENT. |WARNING|
--]]
local Workspace = game:GetService("Workspace")

local HierarchyBuilder = require(Workspace.HierarchyBuilder)


return {
	ParameterGenerator = function()
		return
	end,

	BeforeAll = function() end,
	AfterAll = function() end,
	BeforeEach = function() end,
	AfterEach = function() end,

	Functions = {
		["A"] = function(Profiler)
			HierarchyBuilder{ Name = "test";
				Type = "Part";
				Parent = workspace;

				{ Type = "Folder";
					Name = "test2";

					{ Name = "asdas";
						Type = "ImageLabel";
						Size = UDim2.fromScale(1,1);
					}
				}
			}
		end,

		["B"] = function(Profiler)
			local a = Instance.new("Part")
			a.Name = "test"

			local g = Instance.new("Folder")
			g.Name = "test2"
			g.Parent = a

			local b = Instance.new("ImageLabel")
			b.Name = "asdas"
			b.Size = UDim2.fromScale(1,1)
			b.Parent = g

			a.Parent = workspace	
		end,
	},
}

A; HierarchyBuilder
B; Default Way

2 Likes

1.2.2

  • Added new properties and capabilities
  • Optimized certain parts of the code
  • Discarded the new types for simpler ones

If you question how to use new abilities, refer to GitHub Guide.md

Usually, you won’t only use these during initialization, so it basically just runs once at the beginning.
So taking a few extra milliseconds is negligeable.

1.2.3

  • Changed the way _base works; now it supports hierarchy specific properties like _init _count or even nested _base, allowing for complex inheritance chains and styling
1 Like

1.3.2

  • Added _base
    • This allows you to have complex inheritance chains
  • Added _exec
    • This replaces certain use cases of _init making it less hard-coded
  • Changed _init
    • Now always has a second argument
  • Changed Type
    • Now can accept another hierarchy reinforcing the inheritance capabilities
  • Optimized property setting
  • Reformatted the forum post and docs
  • Removed and deprecated the old Roblox download. Now to get the latest release you should instead pull it from the github

Hey, I’m actually making a plugin about converting hierarchy of instances into code, and I’m going to add your hierarchy builder if you don’t mind.


1 Like

Shouldn’t Type be called Class instead?

It’s a design quirk that will remain until the last update of hierarchy builder. The reason is legacy support, as changing Type to ClassName would ruin that compatibility. If you want you can always change the source to be using ClassName for your project

Beta 1.4.0

  • Added _attach
  • Added _event
  • Minor code optimizations

I’m not understanding the benefits of using this resource and what it brings to the table. The claim that it provides Reactiveness isn’t really grounded.

Sure its a declarative DSL, but it’s purely static. The instances aren’t created based on dynamic state, instead you’re having to break the declarative pattern to introduce state management.
Meanwhile the primary benefit of using these frameworks is for state to also be declarative:

local message = Value("Hello there!")

local ui = New "TextLabel" {
    Name = "Greeting",
    Parent = PlayerGui.ScreenGui,

    Text = message
}

print(ui.Name) --> Greeting
print(ui.Text) --> Hello there!

message:set("Goodbye friend!")
task.wait() -- important: changes are applied on the next frame!
print(ui.Text) --> Goodbye friend!

New Instances - Fusion

Instead, this is how you could achieve the same with this resource:

local message = Instance.new('StringValue')
message.Value = 'Hello there!'

local ui = buildInstance{
	Name = "Greeting",
	Type = "TextLabel",
	Parent = PlayerGui.ScreenGui,
	
	Text = message.Value,
	--this is where the previous example had ended declaring.
	--From now onwards I have to implement
	--the propagation for the declared state,
	--imperatively

	_init = function(self: TextLabel)
		self.Text = message.Value
		--have to bind to changes
		message.Changed:Connect(function(value: string)
			self.Text = value
		end)
		--i would also have to handle cleanup
		--as this connection is a memory leak
	end,
}

print(ui.Name) --> Greeting
print(ui.Text) --> Hello there!

message.Value = 'Goodbye friend!'
task.wait()
print(ui.Text) --> Goodbye friend!

This is the boilerplate work that “Reactiveness” attempts to reduce. Picture doing this for every property in your instance that has a state dependency, across an entire codebase.


Note that this doesn’t mean that static DSL is useless. But if merely for this purpose, then this resource might be too much.

Observe this implementation of static DSL from the 2007 Linked Sword

function Create(ty)
	return function(data)
		local obj = Instance.new(ty)
		for k, v in pairs(data) do
			if type(k) == 'number' then
				v.Parent = obj
			else
				obj[k] = v
			end
		end
		return obj
	end
end

although this code is definetly more recent than 2007

Thats the entire thing, which looks like this practice:

Create "TextLabel" {
	Name = 'Greeting',
	Parent = PlayerGui.ScreenGui,
	Text = "Hello!",
	
	--it supports children aswell
	Create "UICorner" {
		CornerRadius = UDim.new(.15,0)
	}
}

The code you showed is quite inefficient and can be done via _base. Which will remove much of the boilerplate across the codebase. In earlier versions it was true, but early on the HB’s purpose was a bit different.

I created this resource to improve upon the existing D.S.L. models, like you mentioned. If you were to read the documentation, you would see exactly what new it brings to the table.

Also, I don’t understand your mindset with “static is antonym to reactive”. If going by the definition, HB does provide reactive hooks like _init, _exec or in the beta branch _event⁣. Being static doesn’t mean it can’t be reactive; it only requires a different mindset. The main benefit of my resource is how lightweight it is. No matter the framework you compare it to, and especially for what it provides, it will come as the lightest and fastest.

Sure, unlike actual frameworks, HB is quite primitive, and a lot of stuff needs to be implemented manually. But it’s intended to give you control over what you actually need in your project. It will not bloat your code from day 1. Instead, you will gradually build around it to provide exactly what you need.

For anyone who is coming from my friend’s post:

  1. HB is not a full-on replacement for any kind of framework out there. Quite the opposite, it was meant to assist with other development workflows
  2. I plan to develop HB into a fully reactive framework. But it will be on my own terms of staying lightweight and performant. And also will most likely be a separate branch.
  3. DSLs and element-based frameworks are different by design. But both can provide the same power. Do not ground one or another simply for being different.