Page 1 of 2

How to return any variable type?

Posted: Sat Jul 06, 2013 10:56 am
by c4s
With the recent new features of PB and myself being in a productive mood, I finally decided to do some major restructuring / clean-up of my older code.
One thing I want to achieve is to gather a lot of different variables (in my case also known as "settings" or "options") in to a central place, e.g. a structured array. It will include information such as the type (#PB_Byte, #PB_String etc.) and of course the value itself.

The problem is how to properly deal with the set/get procedures? If I would only allow integer, byte etc. this wouldn't be an issue. However I also want to support float and even string "values".

Long story short, I'm thinking of something like this:

Code: Select all

Procedure SettingsInit()
  Settings(#Option1Byte)\Type = #PB_Byte
  Settings(#Option4String)\Type = #PB_String
  ; ...
EndProcedure

Macro SettingsSet(ID, Value)  ; Macro, otherwise 'Value' couldn't have any type
  Select Settings(ID)\Type
    Case #PB_Byte : Settings(ID)\ValueB = Value
    Case #PB_String : Settings(ID)\ValueS = Value
  EndSelect
EndMacro

Procedure SettingsGet(ID)
  Select Settings(ID)\Type
    Case #PB_Byte : Value = Settings(ID)\ValueB
    Case #PB_String : Value = Settings(ID)\ValueS
  EndSelect
  ProcedureReturn Value
EndProcedure


SettingsInit()  ; Setup of the internals

SettingsSet(#Option1Byte, 123)
SettingsSet(#Option4String, "test")

Byte = SettingsGet(#Option1Byte)  ; Should put 123 in 'Byte'
String$ = SettingsGet(#Option4String)  ; Should put "test" in 'String$'
Obviously (and unfortunately) this code doesn't work. Any ideas how I can make it work while keeping the syntax as easy as possible?

Re: How to return any variable type?

Posted: Sat Jul 06, 2013 12:35 pm
by TI-994A
c4s wrote:One thing I want to achieve is to gather a lot of different variables (in my case also known as "settings" or "options") in to a central place, e.g. a structured array. It will include information such as the type (#PB_Byte, #PB_String etc.) and of course the value itself.
Hi c4s. Perhaps the PureBasic function TypeOf() might be useful in determining the variable type. It returns an integer with values including #PB_Byte, #PB_Long, #PB_String, etc.

However, you'd probably have to use some common variable type to handle the values, unless the holding structure contains a field for every variable type.

Re: How to return any variable type?

Posted: Sat Jul 06, 2013 1:01 pm
by said
I think you need to isolate the string case, for other value you can rely on PB auto casting ... something like that:

Code: Select all

#Option1Byte  = 1
#Option2Long  = 2
#Option3Float = 3
#Option4String  = 4


Structure myStruct
   Type.i
   ValueB.b
   ValueL.b
   ValueF.f
   
   ValueS.s
EndStructure

Global Dim Settings.myStruct(10)

Procedure SettingsInit()
  Settings(#Option1Byte)\Type = #PB_Byte
  Settings(#Option4String)\Type = #PB_String
  ; ...
EndProcedure

Macro SettingsSet(ID, Value)  ; Macro, otherwise 'Value' couldn't have any type
  Select Settings(ID)\Type
    Case #PB_Byte : Settings(ID)\ValueB = Value
    Case #PB_Long : Settings(ID)\ValueL = Value
    Case #PB_Float : Settings(ID)\ValueF = Value
  EndSelect
EndMacro
Macro SettingsSetStr(ID, Value)
  Select Settings(ID)\Type
    Case #PB_String : Settings(ID)\ValueS = Value
  EndSelect
EndMacro

Macro SettingsGet(ID, Value)
  Select Settings(ID)\Type
    Case #PB_Byte : Value = Settings(ID)\ValueB
    Case #PB_Long : Value = Settings(ID)\ValueL
  EndSelect
EndMacro

Macro SettingsGetStr(ID, Value)
  Select Settings(ID)\Type
    Case #PB_String : Value = Settings(ID)\ValueS
  EndSelect
EndMacro

SettingsInit()  ; Setup of the internals


SettingsSet(#Option1Byte, 123)
SettingsSetStr(#Option4String, "test")

;Byte = SettingsGet(#Option1Byte)  ; Should put 123 in 'Byte'
;String$ = SettingsGet(#Option4String)  ; Should put "test" in 'String$'

SettingsGet(#Option1Byte, Byte)  ; Should put 123 in 'Byte'
SettingsGetStr(#Option4String, String$)  ; Should put "test" in 'String$'

Debug Byte
Debug String$


Re: How to return any variable type?

Posted: Sat Jul 06, 2013 5:42 pm
by c4s
Thank you two for your suggestions.
TI-994A wrote:you'd probably have to use some common variable type to handle the values, unless the holding structure contains a field for every variable type.
Currently that's the way I've implemented it because I didn't came up with a better solution. It really isn't the best idea to store a couple of variables from which I'm only going to use one. But I'm fine with the little overhead for the sake of easiness.
said wrote:I think you need to isolate the string case
I just found out that when I use a number higher than 255 there will also be compiler error too, because with the macro the high number will be set to the byte variable (at least the compiler thinks so).
It seems that the macro cannot be used at all so I would have to use a set/get procedure for every variable type, not just strings.


That's what I feared of. As soon as I have to take care of special cases, the whole system is getting more complex and that's what I was trying to reduce.
Isn't there maybe some macro magic that can help me out? Or any other ideas on how to get it working? :?

Re: How to return any variable type?

Posted: Sat Jul 06, 2013 7:40 pm
by luis
Maybe it's not something you will like, but would be really a problem to use a specific Set() and Get() proc for each data type ?
After all you should know what type of data are you storing, and hopefully what are you getting back.
You could still store all the settings together if you can just use strings.

Here is what I mean:

Code: Select all

Structure Item
 DataType.i
 s.s
EndStructure : Global Dim Settings.Item(10)


Procedure SettingsSetI (ID, Value.i) 
 Settings(ID)\DataType = #PB_Integer : Settings(ID)\s = Str(Value)
EndProcedure

Procedure SettingsSetS (ID, Value.s) 
 Settings(ID)\DataType = #PB_String: Settings(ID)\s = Value
EndProcedure

Procedure SettingsSetF (ID, Value.f) 
 Settings(ID)\DataType = #PB_Float : Settings(ID)\s = StrF(Value)
EndProcedure

; etc. etc.


Procedure.i SettingsGetI (ID) 
  ProcedureReturn Val(Settings(ID)\s)
EndProcedure

Procedure.f SettingsGetF (ID) 
  ProcedureReturn ValF(Settings(ID)\s)
EndProcedure

Procedure.s SettingsGetS (ID) 
  ProcedureReturn Settings(ID)\s
EndProcedure

; etc. etc.



; TEST

SettingsSetI (0, 123)
SettingsSetS (1, "Hello")
SettingsSetF (2, 123.125)

Debug SettingsGetI(0)
Debug SettingsGetS(1)
Debug SettingsGetF(2)


; and if later on you don't know the data type you can check it
For i = 0 To 2
    Select Settings.Item(i)\DataType
        Case #PB_Integer : Debug SettingsGetI(i)
        Case #PB_String : Debug SettingsGetS(i)
        Case #PB_Float : Debug SettingsGetF(i)    
    EndSelect
Next
I know it's not ideal, but maybe it can do ? This way you don't need the init part either, something I didn't like very much.

Re: How to return any variable type?

Posted: Sat Jul 06, 2013 8:09 pm
by c4s
luis, you're right. I had a second thought about the get/set procedures and realized that having one for each type is probably the best (and only possible) solution - although I still think that one for all would be way more elegant! ;)

By the way: I initiate each option with an unique id because once set, it shouldn't be possible to change the type anymore. Also I'm storing some other information for validation purposes, e.g. what values the option can have etc.

Re: How to return any variable type?

Posted: Sat Jul 06, 2013 9:32 pm
by said
Usually i would go as per Luis's suggested procedures! In case you still want your original way (which is somehow more elegant), you can use this (on more try)

Code: Select all

#Option1Byte  = 1
#Option2Long  = 2
#Option3Float = 3
#Option4Integer  = 4
#Option5Double  = 5
#Option6String  = 6


Structure myStruct
   Type.i
   mem.b[7]    ; to store largest numerical value 8 for double/quad
   String.s
EndStructure

Global Dim Settings.myStruct(10)

Procedure SettingsInit()
  Settings(#Option1Byte)\Type = #PB_Byte
  Settings(#Option2Long)\Type = #PB_Long
  Settings(#Option3Float)\Type = #PB_Float
  Settings(#Option4Integer)\Type = #PB_Integer
  Settings(#Option5Double)\Type = #PB_Double
  Settings(#Option6String)\Type = #PB_String
  ; ...
EndProcedure

Macro SettingsSet(ID, Value)
   Select Settings(ID)\Type
      Case #PB_Byte : PokeB(@Settings(ID)\mem[0], Value)
      Case #PB_Long : PokeL(@Settings(ID)\mem[0], Value)
      Case #PB_Float : PokeF(@Settings(ID)\mem[0], Value)
      Case #PB_Integer : PokeI(@Settings(ID)\mem[0], Value)
      Case #PB_Double : PokeD(@Settings(ID)\mem[0], Value)
   EndSelect
EndMacro

Procedure SettingsSetStr(ID, S.s)
  Select Settings(ID)\Type
    Case #PB_String : Settings(ID)\String = S
  EndSelect
EndProcedure

Macro SettingsGet(ID, Value)
   Select Settings(ID)\Type
    Case #PB_Byte : Value = PeekB(@Settings(ID)\mem[0])
    Case #PB_Long : Value = PeekL(@Settings(ID)\mem[0])
    Case #PB_Float : Value = PeekF(@Settings(ID)\mem[0])
    Case #PB_Integer : Value = PeekI(@Settings(ID)\mem[0])
    Case #PB_Double : Value = PeekD(@Settings(ID)\mem[0])
  EndSelect
EndMacro

Macro SettingsGetStr(ID, Value)
  Select Settings(ID)\Type
    Case #PB_String : Value = Settings(ID)\String
  EndSelect
EndMacro

SettingsInit()  ; Setup of the internals

SettingsSet(#Option1Byte, 123)
SettingsSet(#Option2Long, 12345)
SettingsSet(#Option3Float, 12345.123)
SettingsSet(#Option5Double, 12345678987654.123)
SettingsSetStr(#Option6String, "test")

SettingsGet(#Option1Byte, Byte)
SettingsGet(#Option2Long, Long)
SettingsGet(#Option3Float, Float.f)
SettingsGet(#Option5Double, Double.d)
SettingsGetStr(#Option6String, String$)

Debug Byte
Debug Long
Debug StrF(Float)
Debug StrD(Double)
Debug String$


Re: How to return any variable type?

Posted: Sat Jul 06, 2013 9:56 pm
by TI-994A
c4s wrote:...I still think that one for all would be way more elegant! ;)
Hello again c4s. This method implements your elegant one-for-all preference. When setting the values, to circumvent the multiple-type issue, strings are passed by address, and numerics are all passed as floats and re-cast into their intended types within the procedure. When getting the values, they are all passed as strings, and can be converted to their respective types using the Val() & ValF() functions if required:

Code: Select all

Structure variableInfo
  type.i
  byteVal.b
  longVal.l
  floatVal.f
  stringVal.s
EndStructure

Global Dim varInfo.variableInfo(10)

Procedure setValue(index.i, value.f, type.i)
  Select type
    Case #PB_Byte
      varInfo(index)\byteVal = value
    Case #PB_Long
      varInfo(index)\longVal = value
    Case #PB_Float
      varInfo(index)\floatVal = value
    Case #PB_String
      varInfo(index)\stringVal = PeekS(value)
  EndSelect
  varInfo(index)\type = type
EndProcedure

Procedure.s getValue(index.i)
  Select varInfo(index)\type
    Case #PB_Byte
      ProcedureReturn Str(varInfo(index)\byteVal)
    Case #PB_Long
      ProcedureReturn Str(varInfo(index)\longVal)
    Case #PB_Float
      ProcedureReturn StrF(varInfo(index)\floatVal)
    Case #PB_String
      ProcedureReturn varInfo(index)\stringVal
  EndSelect
EndProcedure

setValue(1, 123, #PB_Byte)
setValue(2, 456, #PB_Long)
setValue(3, 789.123, #PB_Float)
setValue(4, @"Hello", #PB_String)

Debug getValue(1)
Debug getValue(2)
Debug getValue(3)
Debug getValue(4)
Hope it's helpful.

Re: How to return any variable type?

Posted: Sat Jul 06, 2013 10:20 pm
by luis
Problem is this way you just moved the explicit conversions from inside the type-specific procs to the extern of a more generic one. Since you still need to convert the return values to store them in the appropriate variables of the right type in the end the interface is still not uniform but in a different way. And the data structure is now larger (you could use a union for the numeric types but wouldn't help much). I understand is more similar to what c4s hoped, but I don't know if it's really an improvement :mrgreen:


This thread gave me an unhealthy idea -> http://www.purebasic.fr/english/viewtop ... =3&t=55335

Re: How to return any variable type?

Posted: Sat Jul 06, 2013 11:23 pm
by idle
using a union is probably the way to go

Code: Select all

Structure uAny 
   type.i
   StructureUnion 
      a.a
      b.b
      c.c
      u.u
      w.w
      l.l
      i.i
      f.f
      d.d
      q.q
      *ptr
   EndStructureUnion
   s.s
EndStructure   

Debug SizeOf(uany) 

Global Dim Settings.uany(10) 

;set 
For a = 1 To 4 
   Settings(a)\type = #PB_Integer 
   Settings(a)\i = a  
Next 
Settings(5)\type = #PB_Float 
Settings(5)\f = #PI 
For a = 6 To 10 
   Settings(a)\type = #PB_String 
   Settings(a)\s = "string " + Str(a) 
Next 

;enum 
For a = 1 To 10 
   Select Settings(a)\type 
      Case #PB_Integer 
         Debug "Int " + Str(Settings(a)\i) 
      Case #PB_Float 
         Debug StrF(Settings(a)\f) 
      Case #PB_String 
         Debug Settings(a)\s 
   EndSelect 
Next    

;strings need to be passed by address in the macro 
Macro SetSetting(id,val,type) 
   Select type 
      Case #PB_String 
         settings(id)\s = PeekS(val) 
      Default    
         settings(id)\q = val 
   EndSelect 
EndMacro    

;just use the native type 
Macro GetSetting(ID,type) 
   settings(id)\type 
EndMacro 

SetSetting(1,@"hello",#PB_String) 
SetSetting(2,123,#PB_Byte) 

s1.s = GetSetting(1,s) 
Debug s1 
b1.b = GetSetting(2,b)
Debug b1

Re: How to return any variable type?

Posted: Sat Jul 06, 2013 11:31 pm
by luis
Even if it's nice, I still think it doesn't change much.
This time it's not the return value that need to be converted, but it's the GetSetting() proc/macro requiring the name of the field. So again the interface is not uniform, and doesn't give a real benefit vs the multiple typed-procs.
The "s", "i", "b" strings are moved from the name of the proc to one of its params.
JMHO

Re: How to return any variable type?

Posted: Sat Jul 06, 2013 11:35 pm
by c4s
@said & @TI-994A & @idle
Thanks again for your suggestions! Really appreciated as it gives my another perspective on the problem. Especially idle's version looks interesting as it's almost doing what I want.
Written before I've seen idle's code: However, for now I decided to go with a get/set procedure for each type because for me it's even more important to be able to get the value as a direct result:

Code: Select all

; Would be cool:
Byte.b = SettingsGet(#OptionDoThis)
String$ = SettingsGet(#OptionFilePath)

; Also ok, as there is no other way:
Byte.b = SettingsGetB(#OptionDoThis)
String$ = SettingsGetS(#OptionFilePath)
@luis
I really like that feature request you've posted. Would solve my problem while keeping everything PB-like...

Re: How to return any variable type?

Posted: Sun Jul 07, 2013 10:52 am
by TI-994A
luis wrote:...you still need to convert the return values to store them in the appropriate variables of the right type in the end the interface is still not uniform but in a different way. And the data structure is now larger...
Hi luis. You're right, and apparently, that's not what he needed either. My thought was that since the original values were already directly accessible from the global structure, he only needed a convenient way to read the values, in which case the string solution would have sufficed. And although the structure could become quite bloated, especially with the addition of yet more data types, I usually prefer to keep numericals in properly typed variables, maintaining better accuracy, and avoiding slow string and conversion operations where possible.

Re: How to return any variable type?

Posted: Sun Jul 07, 2013 10:53 am
by TI-994A
c4s wrote:...it's even more important to be able to get the value as a direct result:

Code: Select all

; Would be cool:
Byte.b = SettingsGet(#OptionDoThis)
String$ = SettingsGet(#OptionFilePath)
Hi c4s. The main aim of my earlier implementation was to get the values from the structure without having to know or indicate the data type when calling the getValue() function. However, since it returned only a string value, while you clearly need the actual value, perhaps this method might prove more in line with your one-for-all preference. It uses a set of global variable types that would be assigned the corresponding index value of the data structure:

Code: Select all

EnableExplicit

Enumeration
  #OptionDoThis
  #OptionFilePath
  #OptionDoLong
  #OptionDoFloat 
  #OptionFloat2
  #OptionString2
EndEnumeration

Structure variableInfo
  type.i
  byteVal.b
  longVal.l
  floatVal.f
  stringVal.s
EndStructure

Global Dim varInfo.variableInfo(10)
Global byteVal.b, longVal.l, floatVal.f, stringVal.s

Procedure clearVals(); good practice, but not required
  byteVal = 0
  longVal = 0
  floatVal = 0
  stringVal = ""
EndProcedure

Procedure setValue(index.i, value.f, type.i)
  Select type
    Case #PB_Byte
      varInfo(index)\byteVal = value
    Case #PB_Long
      varInfo(index)\longVal = value
    Case #PB_Float
      varInfo(index)\floatVal = value
    Case #PB_String
      varInfo(index)\stringVal = PeekS(value)
  EndSelect
  varInfo(index)\type = type
EndProcedure

Procedure getValue(index.i)
  clearVals()
  Select varInfo(index)\type
    Case #PB_Byte
      byteVal = varInfo(index)\byteVal
    Case #PB_Long
      longVal = varInfo(index)\longVal
    Case #PB_Float
      floatVal = varInfo(index)\floatVal
    Case #PB_String
      stringVal = varInfo(index)\stringVal
  EndSelect
EndProcedure

setValue(#OptionDoThis, 123, #PB_Byte)
setValue(#OptionDoLong, 456, #PB_Long)
setValue(#OptionDoFloat, 789.123, #PB_Float)
setValue(#OptionFilePath, @"Hello", #PB_String)
setValue(#OptionFloat2, 987.654321, #PB_Float)
setValue(#OptionString2, @"Another String", #PB_String)

;according to your requirements, since
;the variable TYPE for getValue() is
;known beforehand, the value could simply
;be assigned to a common global variable
;to be used directly without any conversion

getValue(#OptionDoThis): Debug byteVal
getValue(#OptionDoLong): Debug longVal
getValue(#OptionDoFloat): Debug floatVal
getValue(#OptionFilePath): Debug stringVal
getValue(#OptionString2): Debug stringVal
getValue(#OptionFloat2): Debug floatVal
Just another approach to your final solution.

Re: How to return any variable type?

Posted: Sun Jul 07, 2013 12:56 pm
by TI-994A
Here's an alternate method, utilising only macros, with no type conversions whatsoever:

Code: Select all

EnableExplicit

Enumeration
  #OptionDoThis
  #OptionFilePath
  #OptionDoLong
  #OptionDoFloat 
  #OptionFloat2
  #OptionString2
EndEnumeration

Structure variableInfo
  type.i
  byteVal.b
  longVal.l
  floatVal.f
  stringVal.s
EndStructure

Global Dim varInfo.variableInfo(10)
Define byteVal.b, longVal.l, floatVal.f, stringVal.s

Macro clearVals(); good practice, but not required
  byteVal = 0
  longVal = 0
  floatVal = 0
  stringVal = ""
EndMacro

Macro setValue(index, value, type) 
  CompilerSelect type
    CompilerCase #PB_Byte
      varInfo(index)\byteVal = value
    CompilerCase #PB_Long
      varInfo(index)\longVal = value
    CompilerCase  #PB_Float
      varInfo(index)\floatVal = value
    CompilerCase #PB_String   
      varInfo(index)\stringVal = value
  CompilerEndSelect 
EndMacro 

Macro getValue(index, value) 
  clearVals()
  CompilerSelect TypeOf(value)
    CompilerCase #PB_Byte
      value = varInfo(index)\byteVal
    CompilerCase #PB_Long
      value = varInfo(index)\longVal
    CompilerCase  #PB_Float
      value = varInfo(index)\floatVal
    CompilerCase #PB_String   
      value = varInfo(index)\stringVal
  CompilerEndSelect 
EndMacro 

setValue(#OptionDoThis, 123, #PB_Byte)
setValue(#OptionDoLong, 456, #PB_Long)
setValue(#OptionDoFloat, 789.123, #PB_Float)
setValue(#OptionFilePath, "Hello", #PB_String)
setValue(#OptionFloat2, 987.654321, #PB_Float)
setValue(#OptionString2, "Another String", #PB_String)

getValue(#OptionDoThis, byteVal): Debug byteVal
getValue(#OptionDoLong, longVal): Debug longVal
getValue(#OptionDoFloat, floatVal): Debug floatVal
getValue(#OptionFilePath, stringVal): Debug stringVal
getValue(#OptionString2, stringVal): Debug stringVal
getValue(#OptionFloat2, floatVal): Debug floatVal