![](/i/fill.gif) |
![](/i/fill.gif) |
|
![](/i/fill.gif) |
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
On 5/14/2011 0:53, Warp wrote:
> Invisible<voi### [at] dev null> wrote:
>> 1. Writing
>
>> Iterator<Foo> it = list1.iterator();
>> ArrayList<Foo> list2 = new ArrayList<Foo>();
>> while (it.hasNext()) list2.add(bar(it.next()));
>
>> is way more work than writing
>
>> list2 = map bar list1
>
> Argument from verbosity is seldom a good argument in programming.
> Just because something is shorter doesn't necessarily mean it's better.
I would say in this case it is, because all the extra stuff is just
verbiage. It obscures the algorithm you're actually interested in, which is
to make a new list where each element is the old list with 'bar' applied to
it. All the stuff with the iterator and the types and all is just junk you
have to repeat wherever you need to do this.
--
Darren New, San Diego CA, USA (PST)
"Coding without comments is like
driving without turn signals."
Post a reply to this message
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
On 5/14/2011 7:53, Orchid XP v8 wrote:
> foreign import stdcall "windows.h PostMessageW"
> postMessage :: HWND -> WindowMessage -> WPARAM -> LPARAM -> IO LRESULT
Oh yea. Very nice, yes. Amazing how many of the pre-C# languages made such
a thing so tedious.
> How this actually finds the necessary code to execute, I have no idea.
That's why they call it "the registry".
> You cannot access Haskell data from C at all. You can access C data from
> Haskell if you're careful.
OK. That's what I figured. I couldn't imagine how C was getting to lazy
data easily.
> Memory managemet is fun. (Principly because if you get it wrong... well, you
> know what happens.) There are Haskell libraries for allocating and freeing
> values that the GC doesn't move around (so-called "pinned memory").
In C#, you pin the memory from C# and then invoke your C code. Similar idea.
> You can also arrange for the main() function to be implemented in C rather
> than Haskell. (I believe you have to make sure main() calls some Haskell
> startup functions though.) In this way, you can build a Haskell program that
> calls C, or a C program that calls Haskell.
Yep. Ada even standardizes the names of the functions for this stuff.
> You can also build Haskell DLLs. I have no idea how that works...
>
> Speaking of which, GHC now supports compiling each Haskell library as a DLL,
> rather than linking them all statically. Unfortunately, this is broken
> out-of-the-box. If you compile this way, you have to manually find all the
> DLLs your program requires and move them into the search path, because the
> GHC installer doesn't do this for you. Oops!
That's why they call it the registry. :-)
> The *abstraction* is simple. The *implementation* is not. The same is true
> of many, many things. ;-)
It's simple after it's pointed out, methinks. $1 - hammer tap. $99 - knowing
where to tap.
> Wouldn't converting one gigabyte of data to a new format online take just as
> long as reloading it from disk?
I don't know what you mean. You have to get it off the disk and into memory,
if you've terminated the process and start it up again.
And yes, it would take just as long, but you don't have to do it all at
once. You can be servicing requests, and reformatting data while you're not
servicing requests.
> I'm not seeing why you would need to do this with cold code update either.
> You only need to shut down the processes related to the thing you're
> actually changing.
Then you have a bunch of different processes on the same machine fighting
over resources. Sending messages between them means you now have a bunch of
context switches, etc.
>> You can also convert things slowly, using the old data in the new code
>> until all the old code is gone, then slowly work thru the data (perhaps
>> updating it each time it's touched) to the new format.
>
> This is an unavoidable requirement which *must* be met if you want
> distributed processing. Otherwise upgrading the system requires stopping the
> entire distributed network.
Yep. And that's why building a big system like this with static type-erased
data is more difficult than dynamic tagged data.
>>> Yeah. As I say, the current system is entirely predicated around
>>> everybody
>>> having the same code. As soon as that's not true, it's game over.
>>
>> That, I think, is the fundamental problem with systems like Haskell, and
>> the fundamental reason why dynamic typing may very well win in this
>> situation.
>
> No, this is the fundamental problem with a small proof-of-concept
> implementation exploring whether it's even slightly possible to do this.
> Obviously the current implementation does not yet provide everything
> necessary for production use. Nobody is claiming it does.
Sure. I'm just not sure they've proved the concept if they can't handle
that. :-) Sort of like the STM proof of concept in .NET. ;-)
> What is needed - and the documentation makes plain that the implementors are
> quite aware of this - is some way of varifying that the identifiers you send
> actually refer to the same thing. I don't actually know how Erlang manages
> to do this;
Dynamic typing. :-)
--
Darren New, San Diego CA, USA (PST)
"Coding without comments is like
driving without turn signals."
Post a reply to this message
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
Orchid XP v8 <voi### [at] dev null> wrote:
> I'm fairly sure I posted one in the other group a while back. Heck, I
> even got qsort() from stdlib.h to work arbitrary Haskell data types,
> just because Warp said it wasn't possible.
While calling C's qsort() from Haskell is impressive, you still failed
to demonstrate the next challenge, which was to properly handle C bitfields.
So there's still potentially something that Haskell cannot do with C... :P
--
- Warp
Post a reply to this message
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
On 5/15/2011 3:22, Warp wrote:
> which was to properly handle C bitfields.
Given that C doesn't define what order bit fields go in, that seems like a
tall order. You can't technically do that from assembler, either. :-)
Otherwise, it would seem pretty easy to write Haskell code (or any other
language that handles integer math) to just use masks and multiplies and
divides.
--
Darren New, San Diego CA, USA (PST)
"Coding without comments is like
driving without turn signals."
Post a reply to this message
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
On 15/05/2011 05:41 PM, Darren New wrote:
> On 5/15/2011 3:22, Warp wrote:
>> which was to properly handle C bitfields.
>
> Given that C doesn't define what order bit fields go in, that seems like
> a tall order. You can't technically do that from assembler, either. :-)
>
> Otherwise, it would seem pretty easy to write Haskell code (or any other
> language that handles integer math) to just use masks and multiplies and
> divides.
There's a Haskell standard library that offers all the usual bit-level
operators, so it's not like you have to multiply by 2 when what you
really want is a left-shift.
--
http://blog.orphi.me.uk/
http://www.zazzle.com/MathematicalOrchid*
Post a reply to this message
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
On 15/05/2011 11:22 AM, Warp wrote:
> Orchid XP v8<voi### [at] dev null> wrote:
>> I'm fairly sure I posted one in the other group a while back. Heck, I
>> even got qsort() from stdlib.h to work arbitrary Haskell data types,
>> just because Warp said it wasn't possible.
>
> While calling C's qsort() from Haskell is impressive, you still failed
> to demonstrate the next challenge, which was to properly handle C bitfields.
I thought the next level was setjmp()?
> So there's still potentially something that Haskell cannot do with C... :P
If it's really that hard, ask C to do it, and call C from Haskell.
For that matter, C can't access [non-trivial] Haskell data either. If
your C program wants to, say, access the 7th element of a Haskell list,
it has to call Haskell and ask Haskell to fetch it and return something
simple enough for C to comprehend. (The 7th element might not *exist*
yet, for example...)
--
http://blog.orphi.me.uk/
http://www.zazzle.com/MathematicalOrchid*
Post a reply to this message
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
>> Argument from verbosity is seldom a good argument in programming.
>> Just because something is shorter doesn't necessarily mean it's better.
>
> I would say in this case it is, because all the extra stuff is just
> verbiage. It obscures the algorithm you're actually interested in, which
> is to make a new list where each element is the old list with 'bar'
> applied to it. All the stuff with the iterator and the types and all is
> just junk you have to repeat wherever you need to do this.
A far more relevant objection would be "how often does Haskell let you
do less work?"
--
http://blog.orphi.me.uk/
http://www.zazzle.com/MathematicalOrchid*
Post a reply to this message
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
>> foreign import stdcall "windows.h PostMessageW"
>> postMessage :: HWND -> WindowMessage -> WPARAM -> LPARAM -> IO LRESULT
>
> Oh yea. Very nice, yes. Amazing how many of the pre-C# languages made
> such a thing so tedious.
Hey, our language is *great*! Why would you ever want to call anything else?
>> How this actually finds the necessary code to execute, I have no idea.
>
> That's why they call it "the registry".
Well, no. See, if you call a Win32 function, it just works. If you call
a function that you wrote yourself, you have to tell the linker to link
the corresponding object code.
As I understand it, calling Win32 works by calling a C stub function
which does the necessary machine code magic to actually invoke the
Windows kernel, whatever that may be. But I don't see anywhere where
this gets linked in...
>> You cannot access Haskell data from C at all. You can access C data from
>> Haskell if you're careful.
>
> OK. That's what I figured. I couldn't imagine how C was getting to lazy
> data easily.
You *can* do it of course, but it'll probably break with the next
release of the compiler, so nobody does it. Instead, you write Haskell
stub functions to fetch whatever item of data you want, and call that
from C. (Or just design the program the other way around.)
I'm unsure how much runtime overhead is involved in calling between C
and Haskell.
>> You can also build Haskell DLLs. I have no idea how that works...
>>
>> Speaking of which, GHC now supports compiling each Haskell library as
>> a DLL,
>> rather than linking them all statically. Unfortunately, this is broken
>> out-of-the-box. If you compile this way, you have to manually find all
>> the
>> DLLs your program requires and move them into the search path, because
>> the
>> GHC installer doesn't do this for you. Oops!
>
> That's why they call it the registry. :-)
The general opinion among Haskell developers is that Windows is "that
evil platform that we shouldn't have to support". In particular, DLLs
and the registry are both regarded as being impossibly complex to use,
and to be avoided to the maximum extent possible.
Which is a bit like developing software for Linux and insisting that
everybody should avoid using Bash.
>> Wouldn't converting one gigabyte of data to a new format online take
>> just as long as reloading it from disk?
>
> And yes, it would take just as long, but you don't have to do it all at
> once. You can be servicing requests, and reformatting data while you're
> not servicing requests.
OK, fair enough.
>> I'm not seeing why you would need to do this with cold code update
>> either.
>> You only need to shut down the processes related to the thing you're
>> actually changing.
>
> Then you have a bunch of different processes on the same machine
> fighting over resources. Sending messages between them means you now
> have a bunch of context switches, etc.
I don't follow.
>>> You can also convert things slowly, using the old data in the new code
>>> until all the old code is gone, then slowly work thru the data (perhaps
>>> updating it each time it's touched) to the new format.
>>
>> This is an unavoidable requirement which *must* be met if you want
>> distributed processing. Otherwise upgrading the system requires
>> stopping the
>> entire distributed network.
>
> Yep. And that's why building a big system like this with static
> type-erased data is more difficult than dynamic tagged data.
...which is why the Haskell implementation is using dynamic tagged data.
>> No, this is the fundamental problem with a small proof-of-concept
>> implementation exploring whether it's even slightly possible to do this.
>> Obviously the current implementation does not yet provide everything
>> necessary for production use. Nobody is claiming it does.
>
> Sure. I'm just not sure they've proved the concept if they can't handle
> that. :-)
Can't handle it _yet_. They have concrete plans for how they would
implement it, they just haven't yet.
>> What is needed - and the documentation makes plain that the
>> implementors are
>> quite aware of this - is some way of varifying that the identifiers
>> you send
>> actually refer to the same thing. I don't actually know how Erlang
>> manages
>> to do this;
>
> Dynamic typing. :-)
That's not an answer. Haskell is typing values sent over the wire
dynamically too. That doesn't automatically solve the problem.
There are two things to check: data structure type names and function
names. The Erlang answer seems to be:
1. Make no attempt to verify whether data structures match at all. If
the old code excepts data in one format and the new version expects data
in an almost identical yet slightly different format, when the new code
fires up it will just crash due to the data mismatch. Unless you
specifically tested for this and wrote lots of code to make it work.
(By contrast, Haskell might potentially be able to distinguish the old
type and the new type at the type level, if the structure of the type
has actually changed, and issue a compile-time warning.)
2. I have literally no idea how Erlang sends functions over the wire. I
had assumed it just sends the VM executable code over the wire.
Obviously that only works if you're running on a VM. Haskell isn't.
(There *is* a VM, but it's fairly slow, and has various other gotchas.)
Haskell is sending the function name over the wire, but that only works
if the name means the same thing at the other end.
In both cases, it looks like you need a name plus some kind of version
number, or maybe a hash of a description or something, to distinguish
things that the programmer has assigned identical names to...
Post a reply to this message
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
On 5/16/2011 1:06, Invisible wrote:
> Well, no. See, if you call a Win32 function, it just works. If you call a
> function that you wrote yourself, you have to tell the linker to link the
> corresponding object code.
Unless you register it, I expect.
> As I understand it, calling Win32 works by calling a C stub function which
> does the necessary machine code magic to actually invoke the Windows kernel,
> whatever that may be. But I don't see anywhere where this gets linked in...
It's entirely possible the declaration itself generates that code, isn't it?
I'm pretty sure that's how it works in C#: you say "C# XYZ(i) calls
C-languge PDQ with a (short) parameter" and the compiler generates an XYZ
function that casts its argument to a short and jumps to the PDQ function,
basically.
> the compiler, so nobody does it. Instead, you write Haskell stub functions
Yep. Kind of like calling C++ from C.
> Which is a bit like developing software for Linux and insisting that
> everybody should avoid using Bash.
Yep. More like developing distributed programs for Linux and deciding that
sockets are too hard to use.
>>> I'm not seeing why you would need to do this with cold code update
>>> either.
>>> You only need to shut down the processes related to the thing you're
>>> actually changing.
>>
>> Then you have a bunch of different processes on the same machine
>> fighting over resources. Sending messages between them means you now
>> have a bunch of context switches, etc.
>
> I don't follow.
It's less efficient to have the kernel scheduler scheduling the different
processes than to have the Erlang-specific scheduler doing it. That's why
people make 100,000 erlang tasks in one process and doing the same in Linux
will bring the kernel to its knees, if not run you out of memory.
>> Yep. And that's why building a big system like this with static
>> type-erased data is more difficult than dynamic tagged data.
>
> ...which is why the Haskell implementation is using dynamic tagged data.
Yep. And that loses a lot of the benefits of the static typing right there.
> Can't handle it _yet_. They have concrete plans for how they would implement
> it, they just haven't yet.
Fair enough. I look forward to seeing what they come up with.
> 1. Make no attempt to verify whether data structures match at all. If the
> old code excepts data in one format and the new version expects data in an
> almost identical yet slightly different format, when the new code fires up
> it will just crash due to the data mismatch. Unless you specifically tested
> for this and wrote lots of code to make it work.
Right.
Actually, even worse, in Erlang, the likelihood is that you just leave the
mismatched message in the buffer, which then grows until it crashes the
whole machine with no obvious reason. One of the poorly thought-out designs
there, methinks.
> (By contrast, Haskell might potentially be able to distinguish the old type
> and the new type at the type level, if the structure of the type has
> actually changed, and issue a compile-time warning.)
This assumes you know at compile time what types the other side is using,
which also isn't always true. :-)
> 2. I have literally no idea how Erlang sends functions over the wire.
It's out in the erldocs somewhere, but I can't find it after five minutes of
looking around. They specify the wire format of everything, including that.
> assumed it just sends the VM executable code over the wire.
Given that I can write a function on an x86 machine and ship it over to a
program running on a Sparc that has been running since before I bought the
x86 machine, yah, I'd say there's some sort of virtual code being
transfered. :-)
> In both cases, it looks like you need a name plus some kind of version
> number, or maybe a hash of a description or something, to distinguish things
> that the programmer has assigned identical names to...
It depends on whether you're sending a function or the name of the function,
really.
--
Darren New, San Diego CA, USA (PST)
"Coding without comments is like
driving without turn signals."
Post a reply to this message
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
On 16/05/2011 20:18, Darren New wrote:
> On 5/16/2011 1:06, Invisible wrote:
>> Well, no. See, if you call a Win32 function, it just works. If you call a
>> function that you wrote yourself, you have to tell the linker to link the
>> corresponding object code.
>
> Unless you register it, I expect.
Well, no, what I'm saying is that if you call a function that's defined
in (say) windows.h, the linker somehow "finds" it. But if you wrote some
C source code of your own, you have to tell the linker to link in the
object code or it complains it can't find it.
It makes sense that you have to tell the linker where to find your own
code. What I can't figure out is how it somehow "knows" when a function
is from the Win32 API and magically finds it.
(The answer is probably something like "GHC automatically includes the
Win32 files at link time".)
>> As I understand it, calling Win32 works by calling a C stub function
>> which
>> does the necessary machine code magic to actually invoke the Windows
>> kernel,
>> whatever that may be. But I don't see anywhere where this gets linked
>> in...
>
> It's entirely possible the declaration itself generates that code, isn't
> it? I'm pretty sure that's how it works in C#: you say "C# XYZ(i) calls
> C-languge PDQ with a (short) parameter" and the compiler generates an
> XYZ function that casts its argument to a short and jumps to the PDQ
> function, basically.
If I do a "foreign import", the Haskell compiler generates a Haskell
function which calls the corresponding C function. Which is fine if
you're statically linking the C code into your binary. But you don't
link the Windows kernel into your application, do you? It does some
weird magic with processor rings and code gates to enter kernel mode to
run the code.
I'm no expert, but as I say, I was under the impression that the usual
way to access a DLL is to link in a small C stub which defines the code
necessary to dynamically find and execute the exported functions from
the DLL.
>> Which is a bit like developing software for Linux and insisting that
>> everybody should avoid using Bash.
>
> Yep. More like developing distributed programs for Linux and deciding
> that sockets are too hard to use.
LOL! Yeah...
Seriously. Everybody talks about "hey, it's cool. Oh, except on Windows,
where we have to solve DLL Hell. But hey, who actually uses Windows?"
Personally, I thought "DLL hell" went away about 10 years ago...
>>>> I'm not seeing why you would need to do this with cold code update
>>>> either.
>>>> You only need to shut down the processes related to the thing you're
>>>> actually changing.
>>>
>>> Then you have a bunch of different processes on the same machine
>>> fighting over resources. Sending messages between them means you now
>>> have a bunch of context switches, etc.
>>
>> I don't follow.
>
> It's less efficient to have the kernel scheduler scheduling the
> different processes than to have the Erlang-specific scheduler doing it.
> That's why people make 100,000 erlang tasks in one process and doing the
> same in Linux will bring the kernel to its knees, if not run you out of
> memory.
Right. So all you're saying is that OS processes are heavier than Erlang
processes.
One thing Erlang can do (and Haskell can't, easily) is that because
Erlang is a VM, you can have several unrelated Erlang applications
running on the same VM. But to run multiple Haskell applications, you
would have to have multiple copies of the Haskell runtime in action.
Which, as you say, isn't something you want to do too much of.
>>> Yep. And that's why building a big system like this with static
>>> type-erased data is more difficult than dynamic tagged data.
>>
>> ...which is why the Haskell implementation is using dynamic tagged data.
>
> Yep. And that loses a lot of the benefits of the static typing right there.
*sigh* This old argument again.
Yes, the entire program is statically typed, but this one tiny part
where you have to do a simple runtime type check automatically
COMPLETELY DESTROYS EVERY SINGLE ADVANTAGE OF STATIC TYPING!
>> Can't handle it _yet_. They have concrete plans for how they would
>> implement it, they just haven't yet.
>
> Fair enough. I look forward to seeing what they come up with.
I look forward to seeing whether they ever finish it.
(I lose count of how many projects targeting Haskell at the GPU have
been started. The number of them which produced a production-grade end
product is zero.)
>> 1. Make no attempt to verify whether data structures match at all.
>
> Right.
>
> Actually, even worse, in Erlang, the likelihood is that you just leave
> the mismatched message in the buffer, which then grows until it crashes
> the whole machine with no obvious reason. One of the poorly thought-out
> designs there, methinks.
I'll say!
Basically you need to manually make sure you send a version number at
the start of your message exchange, or something like that, and make
sure that all servers and clients you write can handle all versions of
the protocol.
Or design the protocol so that it never needs to change. (Which is not
infeasible if you can send code as part of the protocol I suppose...)
>> (By contrast, Haskell might potentially be able to distinguish the old
>> type
>> and the new type at the type level, if the structure of the type has
>> actually changed, and issue a compile-time warning.)
>
> This assumes you know at compile time what types the other side is
> using, which also isn't always true. :-)
Not really. When you compile v1, you assume that the other side will be
v1 as well. When you compile v2, the compiler checks what you changed,
and warns you if you don't explicitly handle the v1 data (unless it's
identical).
>> 2. I have literally no idea how Erlang sends functions over the wire.
>
> It's out in the erldocs somewhere, but I can't find it after five
> minutes of looking around. They specify the wire format of everything,
> including that.
Yeah, I figured.
The Haskell implementation has a potential advantage in that you can
specify precisely how to serialise stuff if you want to. (Usually to
send a more compact representation by leaving off data which can be
recomputed at the other side, making use of the special structure of the
data to skip parts of it, etc.) Of course, the bit that tells the other
side who the message is for and what type it contains isn't open to
negotiation. ;-)
>> assumed it just sends the VM executable code over the wire.
>
> Given that I can write a function on an x86 machine and ship it over to
> a program running on a Sparc that has been running since before I bought
> the x86 machine, yah, I'd say there's some sort of virtual code being
> transfered. :-)
And yet, that would seem rather heavy compared to just sending a
function name (or rather, a *unique* function identifier), so perhaps
Erlang actually does it that way like Haskell is doing. Or maybe it only
sends the code if the other side doesn't already have it or something.
Come to think of it, I'm still figuring out how it manages to send atoms...
>> In both cases, it looks like you need a name plus some kind of version
>> number, or maybe a hash of a description or something, to distinguish
>> things
>> that the programmer has assigned identical names to...
>
> It depends on whether you're sending a function or the name of the
> function, really.
Well, we're sending type names too, which give you the same problem.
(And sending a type definition doesn't help, because we still need to
figure out whether they're *meant* to be the same type or not.)
Post a reply to this message
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |
|
![](/i/fill.gif) |
| ![](/i/fill.gif) |
|
![](/i/fill.gif) |